From a98b3ee06f94841bcf033dfa43295b886d5dcc90 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 16 Sep 2022 19:05:19 -0700 Subject: [PATCH 001/229] Updated CoreLock for Swift 5.7 --- Package.resolved | 321 +++---------- Package.swift | 92 ++-- Sources/CoreLock/Authentication.swift | 86 ---- .../{ => Bluetooth}/Advertisement.swift | 31 +- .../CoreLock/{ => Bluetooth}/Central.swift | 3 +- .../ConfirmNewKeyCharacteristic.swift | 5 +- .../CreateNewKeyCharacteristic.swift | 11 +- .../{ => Bluetooth}/DeviceManager.swift | 11 +- .../EventsCharacteristic.swift | 1 + .../CoreLock/{ => Bluetooth}/GATTError.swift | 0 .../{ => Bluetooth}/GATTProfile.swift | 1 - .../{ => Bluetooth}/KeysCharacteristic.swift | 13 +- .../ListEventsCharacteristic.swift | 7 +- .../ListKeysCharacteristic.swift | 7 +- .../LockInformationCharacteristic.swift | 8 +- .../RemoveKeyCharacteristic.swift | 7 +- .../{ => Bluetooth}/SetupCharacteristic.swift | 17 +- .../UnlockCharacteristic.swift | 7 +- Sources/CoreLock/Crypto.swift | 63 --- Sources/CoreLock/Crypto/Authentication.swift | 57 +++ .../{ => Crypto}/AuthenticationError.swift | 0 Sources/CoreLock/Crypto/Crypto.swift | 113 +++++ Sources/CoreLock/Crypto/EncryptedData.swift | 37 ++ .../CoreLock/{ => Crypto}/SecureData.swift | 51 +-- Sources/CoreLock/EncryptedData.swift | 45 -- Sources/CoreLock/Event.swift | 42 +- Sources/CoreLock/{ => Extensions}/Bool.swift | 0 .../CoreLock/{ => Extensions}/Integer.swift | 0 Sources/CoreLock/Key.swift | 8 +- Sources/CoreLock/LockAuthorizationStore.swift | 24 +- Sources/CoreLock/LockConfiguration.swift | 6 +- Sources/CoreLock/LockHardware.swift | 16 - Sources/CoreLock/LockModel.swift | 17 +- .../CreateNewKeyRequest.swift | 3 +- .../{ => Networking}/DeleteKeyRequest.swift | 5 +- .../{ => Networking}/EventsRequest.swift | 3 +- .../{ => Networking}/EventsResponse.swift | 3 +- .../{ => Networking}/KeysRequest.swift | 4 +- .../{ => Networking}/KeysResponse.swift | 3 +- .../LockInformationRequest.swift | 3 +- .../LockInformationResponse.swift | 9 +- .../{ => Networking}/LockNetService.swift | 9 +- Sources/CoreLock/{ => Networking}/URL.swift | 0 .../{ => Networking}/URLSession.swift | 0 .../{ => Networking}/UpdateRequest.swift | 3 +- Sources/CoreLock/NewKey.swift | 6 +- Sources/CoreLock/Permission.swift | 8 +- Sources/CoreLock/TLV.swift | 8 +- Sources/CoreLock/UIDevice.swift | 427 ------------------ Sources/CoreLock/UnlockAction.swift | 1 + Sources/CoreLock/Version.swift | 2 +- Sources/CoreLockWebServer/Permissions.swift | 2 +- Sources/lockd/AuthorizationStoreFile.swift | 8 +- Tests/CoreLockTests/GATTProfileTests.swift | 26 +- iOS/LockKit/Controller/Extensions/Setup.swift | 2 +- .../Controller/Extensions/Unlock.swift | 2 +- .../Controller/Extensions/Update.swift | 2 +- .../LockPermissionsViewController.swift | 4 +- .../Controller/LockViewController.swift | 4 +- ...NewKeySelectPermissionViewController.swift | 6 +- .../Protocols/NewKeyViewController.swift | 2 +- iOS/LockKit/Model/Activity.swift | 6 +- iOS/LockKit/Model/ApplicationData.swift | 10 +- iOS/LockKit/Model/BeaconController.swift | 8 +- .../Model/CoreData/ContactManagedObject.swift | 2 +- .../Model/CoreData/EventManagedObject.swift | 2 +- .../Model/CoreData/LockManagedObject.swift | 4 +- .../Model/CoreData/ManagedObject.swift | 4 +- .../RemoveKeyEventManagedObject.swift | 2 +- iOS/LockKit/Model/CoreSpotlight.swift | 2 +- iOS/LockKit/Model/Intent.swift | 6 +- iOS/LockKit/Model/Store.swift | 10 +- iOS/Message/MessagesViewController.swift | 2 +- iOS/QuickLook/PreviewViewController.swift | 2 +- .../Controller/KeysViewController.swift | 2 +- .../Controller/InterfaceController.swift | 2 +- iOS/Watch Extension/Controller/Unlock.swift | 2 +- iOS/Watch Extension/SessionController.swift | 2 +- 78 files changed, 536 insertions(+), 1194 deletions(-) delete mode 100644 Sources/CoreLock/Authentication.swift rename Sources/CoreLock/{ => Bluetooth}/Advertisement.swift (61%) rename Sources/CoreLock/{ => Bluetooth}/Central.swift (99%) rename Sources/CoreLock/{ => Bluetooth}/ConfirmNewKeyCharacteristic.swift (96%) rename Sources/CoreLock/{ => Bluetooth}/CreateNewKeyCharacteristic.swift (92%) rename Sources/CoreLock/{ => Bluetooth}/DeviceManager.swift (99%) rename Sources/CoreLock/{ => Bluetooth}/EventsCharacteristic.swift (99%) rename Sources/CoreLock/{ => Bluetooth}/GATTError.swift (100%) rename Sources/CoreLock/{ => Bluetooth}/GATTProfile.swift (99%) rename Sources/CoreLock/{ => Bluetooth}/KeysCharacteristic.swift (94%) rename Sources/CoreLock/{ => Bluetooth}/ListEventsCharacteristic.swift (90%) rename Sources/CoreLock/{ => Bluetooth}/ListKeysCharacteristic.swift (88%) rename Sources/CoreLock/{ => Bluetooth}/LockInformationCharacteristic.swift (91%) rename Sources/CoreLock/{ => Bluetooth}/RemoveKeyCharacteristic.swift (90%) rename Sources/CoreLock/{ => Bluetooth}/SetupCharacteristic.swift (87%) rename Sources/CoreLock/{ => Bluetooth}/UnlockCharacteristic.swift (90%) delete mode 100644 Sources/CoreLock/Crypto.swift create mode 100644 Sources/CoreLock/Crypto/Authentication.swift rename Sources/CoreLock/{ => Crypto}/AuthenticationError.swift (100%) create mode 100644 Sources/CoreLock/Crypto/Crypto.swift create mode 100644 Sources/CoreLock/Crypto/EncryptedData.swift rename Sources/CoreLock/{ => Crypto}/SecureData.swift (73%) delete mode 100644 Sources/CoreLock/EncryptedData.swift rename Sources/CoreLock/{ => Extensions}/Bool.swift (100%) rename Sources/CoreLock/{ => Extensions}/Integer.swift (100%) rename Sources/CoreLock/{ => Networking}/CreateNewKeyRequest.swift (99%) rename Sources/CoreLock/{ => Networking}/DeleteKeyRequest.swift (98%) rename Sources/CoreLock/{ => Networking}/EventsRequest.swift (99%) rename Sources/CoreLock/{ => Networking}/EventsResponse.swift (99%) rename Sources/CoreLock/{ => Networking}/KeysRequest.swift (99%) rename Sources/CoreLock/{ => Networking}/KeysResponse.swift (99%) rename Sources/CoreLock/{ => Networking}/LockInformationRequest.swift (99%) rename Sources/CoreLock/{ => Networking}/LockInformationResponse.swift (90%) rename Sources/CoreLock/{ => Networking}/LockNetService.swift (97%) rename Sources/CoreLock/{ => Networking}/URL.swift (100%) rename Sources/CoreLock/{ => Networking}/URLSession.swift (100%) rename Sources/CoreLock/{ => Networking}/UpdateRequest.swift (99%) delete mode 100644 Sources/CoreLock/UIDevice.swift diff --git a/Package.resolved b/Package.resolved index 786fb0f4..5ba754f7 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,259 +1,68 @@ { - "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", - "state": { - "branch": "master", - "revision": "b03524c7a0c7eca048133a9df9dc4bbe727b38e0", - "version": null - } - }, - { - "package": "BluetoothDarwin", - "repositoryURL": "https://github.com/PureSwift/BluetoothDarwin.git", - "state": { - "branch": "master", - "revision": "d6c1eb2c2433efdf621aa8fc2d11d271261ca752", - "version": null - } - }, - { - "package": "BluetoothLinux", - "repositoryURL": "https://github.com/PureSwift/BluetoothLinux.git", - "state": { - "branch": "master", - "revision": "52078ccc6928c8f3e7712bbda052f4a927a2e5ad", - "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": "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", - "state": { - "branch": "master", - "revision": "955a19b2030e08666154f2ee52d7955f56bb6ff5", - "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", - "state": { - "branch": "master", - "revision": "b0b23d27632eb9e837c3313cd2df657a9084bc9a", - "version": null - } - }, - { - "package": "TLVCoding", - "repositoryURL": "https://github.com/PureSwift/TLVCoding.git", - "state": { - "branch": "master", - "revision": "8c41d0fd48f6fc00c429aad025c114a941aad764", - "version": null - } - }, - { - "package": "TypeDecoder", - "repositoryURL": "https://github.com/IBM-Swift/TypeDecoder.git", - "state": { - "branch": null, - "revision": "a5978582981f7151594bdaf5fe0d3cd9b1d2d0c0", - "version": "1.3.4" - } + "pins" : [ + { + "identity" : "bluetooth", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/Bluetooth.git", + "state" : { + "revision" : "f28f73c1ff5e0877c212300ab356997e7e9570fa", + "version" : "6.1.0" } - ] - }, - "version": 1 + }, + { + "identity" : "bluetoothlinux", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/BluetoothLinux.git", + "state" : { + "branch" : "master", + "revision" : "ddd5493bd5382ca0132b8ca121dd35c7554ab7d0" + } + }, + { + "identity" : "gatt", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/GATT.git", + "state" : { + "branch" : "master", + "revision" : "579ffd583f9a32a88e68b69e12eb85856ee165cb" + } + }, + { + "identity" : "socket", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/Socket.git", + "state" : { + "branch" : "main", + "revision" : "c5d21b3b37a0fb55abee4c754e96ef552862d8a3" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/swift-system", + "state" : { + "branch" : "feature/dynamic-lib", + "revision" : "3e5be49e7cee5ba46f5b5bc994f08061dd9fe92a" + } + }, + { + "identity" : "swiftygpio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/uraimo/SwiftyGPIO.git", + "state" : { + "branch" : "master", + "revision" : "1754dc31e4d648c6999b3e664d8669d1a87c22eb" + } + }, + { + "identity" : "tlvcoding", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/TLVCoding.git", + "state" : { + "branch" : "master", + "revision" : "548e984056146722f148794a48f18f093c778097" + } + } + ], + "version" : 2 } diff --git a/Package.swift b/Package.swift index 18bed1a7..2c511930 100644 --- a/Package.swift +++ b/Package.swift @@ -1,21 +1,20 @@ -// swift-tools-version:5.0 +// swift-tools-version:5.6 import PackageDescription -#if os(Linux) -let nativeBluetooth: Target.Dependency = "BluetoothLinux" -let nativeGATT: Target.Dependency = "GATT" -#elseif os(macOS) -let nativeBluetooth: Target.Dependency = "BluetoothDarwin" -let nativeGATT: Target.Dependency = "DarwinGATT" -#endif - let package = Package( name: "Lock", + platforms: [ + .macOS(.v10_15), + .iOS(.v13), + .watchOS(.v6), + .tvOS(.v13), + ], products: [ + /* .executable( name: "lockd", targets: ["lockd"] - ), + ),*/ .library( name: "CoreLock", targets: ["CoreLock"] @@ -23,89 +22,68 @@ let package = Package( ], dependencies: [ .package( - url: "https://github.com/krzyzanowskim/CryptoSwift", - .branch("master") - ), - .package( - url: "https://github.com/uraimo/SwiftyGPIO.git", - .branch("master") + url: "https://github.com/PureSwift/Bluetooth.git", + .upToNextMajor(from: "6.0.0") ), .package( url: "https://github.com/PureSwift/TLVCoding.git", - .branch("master") + branch: "master" ), .package( url: "https://github.com/PureSwift/GATT.git", - .branch("master") + branch: "master" ), .package( url: "https://github.com/PureSwift/BluetoothLinux.git", - .branch("master") - ), - .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") + branch: "master" ), .package( - url: "https://github.com/Bouke/NetService.git", - from: "0.7.0" - ), - .package( - url: "https://github.com/PureSwift/Bonjour.git", - .branch("master") + url: "https://github.com/uraimo/SwiftyGPIO.git", + branch: "master" ) ], targets: [ + /* .target( name: "lockd", dependencies: [ - nativeBluetooth, - nativeGATT, "CoreLockGATTServer", "SwiftyGPIO", - "HAP", "CoreLockWebServer" ] - ), + ),*/ .target( name: "CoreLock", dependencies: [ - nativeGATT, "TLVCoding", - "CryptoSwift", - "Bonjour" + "GATT", + .product(name: "Bluetooth", package: "Bluetooth"), ] ), + /* .target( name: "CoreLockGATTServer", dependencies: ["CoreLock"] - ), - .target( - name: "CoreLockWebServer", - dependencies: [ - "CoreLock", - "Kitura" - ] - ), + ),*/ .testTarget( name: "CoreLockTests", dependencies: ["CoreLock"] - ), - .testTarget( - name: "CoreLockGATTServerTests", - dependencies: ["CoreLockGATTServer"] ) ] ) #if os(Linux) -package.targets.first(where: { $0.name == "CoreLockWebServer" })?.dependencies.append("NetService") +package.dependencies.append( + .package( + url: "https://github.com/apple/swift-crypto.git", + .upToNextMajor(from: "2.1.0") + ) +) +package.targets[0].dependencies.append( + .product( + name: "Crypto", + package: "swift-crypto", + condition: .when(platforms: [.linux]) + ) +) #endif diff --git a/Sources/CoreLock/Authentication.swift b/Sources/CoreLock/Authentication.swift deleted file mode 100644 index 9cd76826..00000000 --- a/Sources/CoreLock/Authentication.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// AuthenticationMessage.swift -// CoreLock -// -// Created by Alsey Coleman Miller on 8/11/18. -// - -import Foundation - -public struct Authentication: Equatable, Codable { - - public let message: AuthenticationMessage - - public let signedData: AuthenticationData - - public init(key: KeyData, - message: AuthenticationMessage = AuthenticationMessage()) { - - self.message = message - self.signedData = AuthenticationData(key: key, message: message) - } - - public func isAuthenticated(with key: KeyData) -> Bool { - return signedData.isAuthenticated(with: key, message: message) - } -} - -/// HMAC Message -public struct AuthenticationMessage: Equatable, Codable { - - public let date: Date - - public let nonce: Nonce - - public init(date: Date = Date(), - nonce: Nonce = Nonce()) { - - self.date = date - self.nonce = nonce - } -} - -/// HMAC data -public struct AuthenticationData: Equatable { - - internal static let length = HMACSize - - public let data: Data - - public init?(data: Data) { - - guard data.count == type(of: self).length - else { return nil } - - self.data = data - } - - public init(key: KeyData, message: AuthenticationMessage) { - - self = HMAC(key: key, message: message) - } - - public func isAuthenticated(with key: KeyData, message: AuthenticationMessage) -> Bool { - - return data == AuthenticationData(key: key, message: message).data - } -} - -extension AuthenticationData: Codable { - - public init(from decoder: Decoder) throws { - - let container = try decoder.singleValueContainer() - let data = try container.decode(Data.self) - guard let value = AuthenticationData(data: data) else { - throw DecodingError.typeMismatch(AuthenticationData.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of bytes \(data.count) for \(String(reflecting: AuthenticationData.self))")) - } - self = value - } - - public func encode(to encoder: Encoder) throws { - - var container = encoder.singleValueContainer() - try container.encode(data) - } -} diff --git a/Sources/CoreLock/Advertisement.swift b/Sources/CoreLock/Bluetooth/Advertisement.swift similarity index 61% rename from Sources/CoreLock/Advertisement.swift rename to Sources/CoreLock/Bluetooth/Advertisement.swift index 24e72337..f75864ad 100644 --- a/Sources/CoreLock/Advertisement.swift +++ b/Sources/CoreLock/Bluetooth/Advertisement.swift @@ -5,33 +5,34 @@ // Created by Alsey Coleman Miller on 8/11/18. // +#if os(macOS) || os(Linux) + import Foundation import Bluetooth - - -#if os(macOS) || os(Linux) +import BluetoothHCI +import BluetoothGAP public extension BluetoothHostControllerInterface { /// LE Advertise with iBeacon - func setLockAdvertisingData(lock: UUID, rssi: Int8, commandTimeout: HCICommandTimeout = .default) throws { + func setLockAdvertisingData(lock: UUID, rssi: Int8) async throws { - do { try enableLowEnergyAdvertising(false) } + do { try await enableLowEnergyAdvertising(false) } catch HCIError.commandDisallowed { } let beacon = AppleBeacon(uuid: lock, rssi: rssi) let flags: GAPFlags = [.lowEnergyGeneralDiscoverableMode, .notSupportedBREDR] - try iBeacon(beacon, flags: flags, interval: .min, timeout: commandTimeout) + try await iBeacon(beacon, flags: flags, interval: .min) - do { try enableLowEnergyAdvertising() } + do { try await enableLowEnergyAdvertising() } catch HCIError.commandDisallowed { } } /// LE Scan Response - func setLockScanResponse(commandTimeout: HCICommandTimeout = .default) throws { + func setLockScanResponse() async throws { - do { try enableLowEnergyAdvertising(false) } + do { try await enableLowEnergyAdvertising(false) } catch HCIError.commandDisallowed { } let name: GAPCompleteLocalName = "Lock" @@ -40,23 +41,23 @@ public extension BluetoothHostControllerInterface { let encoder = GAPDataEncoder() let data = try encoder.encodeAdvertisingData(name, serviceUUID) - try setLowEnergyScanResponse(data, timeout: commandTimeout) + try await setLowEnergyScanResponse(data) - do { try enableLowEnergyAdvertising() } + do { try await enableLowEnergyAdvertising() } catch HCIError.commandDisallowed { } } /// LE Advertise with iBeacon for data changed - func setNotificationAdvertisement(rssi: Int8, commandTimeout: HCICommandTimeout = .default) throws { + func setNotificationAdvertisement(rssi: Int8) async throws { - do { try enableLowEnergyAdvertising(false) } + do { try await enableLowEnergyAdvertising(false) } catch HCIError.commandDisallowed { } let beacon = AppleBeacon(uuid: .lockNotificationBeacon, rssi: rssi) let flags: GAPFlags = [.lowEnergyGeneralDiscoverableMode, .notSupportedBREDR] - try iBeacon(beacon, flags: flags, interval: .min, timeout: commandTimeout) + try await iBeacon(beacon, flags: flags, interval: .min) - do { try enableLowEnergyAdvertising() } + do { try await enableLowEnergyAdvertising() } catch HCIError.commandDisallowed { } } } diff --git a/Sources/CoreLock/Central.swift b/Sources/CoreLock/Bluetooth/Central.swift similarity index 99% rename from Sources/CoreLock/Central.swift rename to Sources/CoreLock/Bluetooth/Central.swift index 72e5d84e..230fb4d2 100644 --- a/Sources/CoreLock/Central.swift +++ b/Sources/CoreLock/Bluetooth/Central.swift @@ -8,7 +8,7 @@ import Foundation import Bluetooth import GATT - +/* internal extension CentralProtocol { /// Connects to the device, fetches the data, performs the action, and disconnects. @@ -161,3 +161,4 @@ internal struct Timeout { } } } +*/ diff --git a/Sources/CoreLock/ConfirmNewKeyCharacteristic.swift b/Sources/CoreLock/Bluetooth/ConfirmNewKeyCharacteristic.swift similarity index 96% rename from Sources/CoreLock/ConfirmNewKeyCharacteristic.swift rename to Sources/CoreLock/Bluetooth/ConfirmNewKeyCharacteristic.swift index 2273c3f3..8282adef 100644 --- a/Sources/CoreLock/ConfirmNewKeyCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/ConfirmNewKeyCharacteristic.swift @@ -7,6 +7,7 @@ import Foundation import Bluetooth +import GATT /// Used to complete new key creation. public struct ConfirmNewKeyCharacteristic: TLVCharacteristic, Codable, Equatable { @@ -18,7 +19,7 @@ public struct ConfirmNewKeyCharacteristic: TLVCharacteristic, Codable, Equatable public static let properties: Bluetooth.BitMaskOptionSet = [.write] /// Identifier of new key. - public let identifier: UUID + public let id: UUID /// Encrypted payload. public let encryptedData: EncryptedData @@ -27,7 +28,7 @@ public struct ConfirmNewKeyCharacteristic: TLVCharacteristic, Codable, Equatable let requestData = try type(of: self).encoder.encode(request) self.encryptedData = try EncryptedData(encrypt: requestData, with: sharedSecret) - self.identifier = key + self.id = key } public func decrypt(with sharedSecret: KeyData) throws -> ConfirmNewKeyRequest { diff --git a/Sources/CoreLock/CreateNewKeyCharacteristic.swift b/Sources/CoreLock/Bluetooth/CreateNewKeyCharacteristic.swift similarity index 92% rename from Sources/CoreLock/CreateNewKeyCharacteristic.swift rename to Sources/CoreLock/Bluetooth/CreateNewKeyCharacteristic.swift index a9365ba2..b72fba6d 100644 --- a/Sources/CoreLock/CreateNewKeyCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/CreateNewKeyCharacteristic.swift @@ -7,6 +7,7 @@ import Foundation import Bluetooth +import GATT /// Used to create a new key. public struct CreateNewKeyCharacteristic: TLVCharacteristic, Codable, Equatable { @@ -18,7 +19,7 @@ public struct CreateNewKeyCharacteristic: TLVCharacteristic, Codable, Equatable public static let properties: Bluetooth.BitMaskOptionSet = [.write] /// Identifier of key making request. - public let identifier: UUID + public let id: UUID /// Encrypted payload. public let encryptedData: EncryptedData @@ -27,7 +28,7 @@ public struct CreateNewKeyCharacteristic: TLVCharacteristic, Codable, Equatable let requestData = try type(of: self).encoder.encode(request) self.encryptedData = try EncryptedData(encrypt: requestData, with: sharedSecret) - self.identifier = key + self.id = key } public func decrypt(with sharedSecret: KeyData) throws -> CreateNewKeyRequest { @@ -44,7 +45,7 @@ public struct CreateNewKeyCharacteristic: TLVCharacteristic, Codable, Equatable public struct CreateNewKeyRequest: Equatable, Codable { /// New Key identifier - public let identifier: UUID + public let id: UUID /// The name of the new key. public let name: String @@ -63,7 +64,7 @@ public extension CreateNewKeyRequest { init(key: NewKey, secret: KeyData) { - self.identifier = key.identifier + self.id = key.id self.name = key.name self.permission = key.permission self.expiration = key.expiration @@ -75,7 +76,7 @@ public extension NewKey { init(request: CreateNewKeyRequest, created: Date = Date()) { - self.identifier = request.identifier + self.id = request.id self.name = request.name self.permission = request.permission self.expiration = request.expiration diff --git a/Sources/CoreLock/DeviceManager.swift b/Sources/CoreLock/Bluetooth/DeviceManager.swift similarity index 99% rename from Sources/CoreLock/DeviceManager.swift rename to Sources/CoreLock/Bluetooth/DeviceManager.swift index 2675e07c..99122fa5 100644 --- a/Sources/CoreLock/DeviceManager.swift +++ b/Sources/CoreLock/Bluetooth/DeviceManager.swift @@ -8,7 +8,7 @@ import Foundation import Bluetooth import GATT - +/* /// SmartLock GATT Central client. public final class LockManager { @@ -189,7 +189,7 @@ public final class LockManager { } /// Remove the specified key. - public func removeKey(_ identifier: UUID, + public func removeKey(_ id: UUID, type: KeyType = .key, for peripheral: Peripheral, with key: KeyCredentials, @@ -418,12 +418,12 @@ public struct LockPeripheral : Equatable { public struct KeyCredentials: Equatable { - public let identifier: UUID + public let id: UUID public let secret: KeyData - public init(identifier: UUID, secret: KeyData) { - self.identifier = identifier + public init(id: UUID, secret: KeyData) { + self.id = id self.secret = secret } } @@ -464,3 +464,4 @@ internal final class Semaphore { semaphore.signal() } } +*/ diff --git a/Sources/CoreLock/EventsCharacteristic.swift b/Sources/CoreLock/Bluetooth/EventsCharacteristic.swift similarity index 99% rename from Sources/CoreLock/EventsCharacteristic.swift rename to Sources/CoreLock/Bluetooth/EventsCharacteristic.swift index 487e5fd6..a97a4c77 100644 --- a/Sources/CoreLock/EventsCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/EventsCharacteristic.swift @@ -7,6 +7,7 @@ import Foundation import Bluetooth +import GATT import TLVCoding /// Encrypted list of events. diff --git a/Sources/CoreLock/GATTError.swift b/Sources/CoreLock/Bluetooth/GATTError.swift similarity index 100% rename from Sources/CoreLock/GATTError.swift rename to Sources/CoreLock/Bluetooth/GATTError.swift diff --git a/Sources/CoreLock/GATTProfile.swift b/Sources/CoreLock/Bluetooth/GATTProfile.swift similarity index 99% rename from Sources/CoreLock/GATTProfile.swift rename to Sources/CoreLock/Bluetooth/GATTProfile.swift index 7d356e4c..571e1cb7 100644 --- a/Sources/CoreLock/GATTProfile.swift +++ b/Sources/CoreLock/Bluetooth/GATTProfile.swift @@ -18,7 +18,6 @@ public protocol GATTProfile { public extension GATTProfile { static var characteristics: [GATTProfileCharacteristic.Type] { - return services.reduce([]) { $0 + $1.characteristics } } } diff --git a/Sources/CoreLock/KeysCharacteristic.swift b/Sources/CoreLock/Bluetooth/KeysCharacteristic.swift similarity index 94% rename from Sources/CoreLock/KeysCharacteristic.swift rename to Sources/CoreLock/Bluetooth/KeysCharacteristic.swift index 549ee729..997e35d2 100644 --- a/Sources/CoreLock/KeysCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/KeysCharacteristic.swift @@ -7,6 +7,7 @@ import Foundation import Bluetooth +import GATT import TLVCoding /// Encrypted list of keys. @@ -88,13 +89,13 @@ public extension KeysList { return keys.isEmpty && newKeys.isEmpty } - mutating func remove(_ identifier: UUID, type: KeyType = .key) { + mutating func remove(_ id: UUID, type: KeyType = .key) { switch type { case .key: - keys.removeAll(where: { $0.identifier == identifier }) + keys.removeAll(where: { $0.id == id }) case .newKey: - newKeys.removeAll(where: { $0.identifier == identifier }) + newKeys.removeAll(where: { $0.id == id }) } } } @@ -146,12 +147,12 @@ public extension KeyListNotification { case key(Key) case newKey(NewKey) - public var identifier: UUID { + public var id: UUID { switch self { case let .key(key): - return key.identifier + return key.id case let .newKey(newKey): - return newKey.identifier + return newKey.id } } } diff --git a/Sources/CoreLock/ListEventsCharacteristic.swift b/Sources/CoreLock/Bluetooth/ListEventsCharacteristic.swift similarity index 90% rename from Sources/CoreLock/ListEventsCharacteristic.swift rename to Sources/CoreLock/Bluetooth/ListEventsCharacteristic.swift index 148b5e8a..9ca6960c 100644 --- a/Sources/CoreLock/ListEventsCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/ListEventsCharacteristic.swift @@ -7,6 +7,7 @@ import Foundation import Bluetooth +import GATT /// List events request public struct ListEventsCharacteristic: TLVCharacteristic, Codable, Equatable { @@ -18,7 +19,7 @@ public struct ListEventsCharacteristic: TLVCharacteristic, Codable, Equatable { public static let properties: Bluetooth.BitMaskOptionSet = [.write] /// Identifier of key making request. - public let identifier: UUID + public let id: UUID /// HMAC of key and nonce, and HMAC message public let authentication: Authentication @@ -26,11 +27,11 @@ public struct ListEventsCharacteristic: TLVCharacteristic, Codable, Equatable { /// Fetch limit for events to view. public let fetchRequest: LockEvent.FetchRequest? - public init(identifier: UUID, + public init(id: UUID, authentication: Authentication, fetchRequest: LockEvent.FetchRequest? = nil) { - self.identifier = identifier + self.id = id self.authentication = authentication self.fetchRequest = fetchRequest } diff --git a/Sources/CoreLock/ListKeysCharacteristic.swift b/Sources/CoreLock/Bluetooth/ListKeysCharacteristic.swift similarity index 88% rename from Sources/CoreLock/ListKeysCharacteristic.swift rename to Sources/CoreLock/Bluetooth/ListKeysCharacteristic.swift index e84e5301..f5007e97 100644 --- a/Sources/CoreLock/ListKeysCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/ListKeysCharacteristic.swift @@ -7,6 +7,7 @@ import Foundation import Bluetooth +import GATT /// List keys request public struct ListKeysCharacteristic: TLVCharacteristic, Codable, Equatable { @@ -18,15 +19,15 @@ public struct ListKeysCharacteristic: TLVCharacteristic, Codable, Equatable { public static let properties: Bluetooth.BitMaskOptionSet = [.write] /// Identifier of key making request. - public let identifier: UUID + public let id: UUID /// HMAC of key and nonce, and HMAC message public let authentication: Authentication - public init(identifier: UUID, + public init(id: UUID, authentication: Authentication) { - self.identifier = identifier + self.id = id self.authentication = authentication } } diff --git a/Sources/CoreLock/LockInformationCharacteristic.swift b/Sources/CoreLock/Bluetooth/LockInformationCharacteristic.swift similarity index 91% rename from Sources/CoreLock/LockInformationCharacteristic.swift rename to Sources/CoreLock/Bluetooth/LockInformationCharacteristic.swift index 86bdae8b..d52be6e4 100644 --- a/Sources/CoreLock/LockInformationCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/LockInformationCharacteristic.swift @@ -6,7 +6,7 @@ // import Foundation -import Bluetooth +import GATT /// Used to determine identity, compatibility and supported features. public struct LockInformationCharacteristic: TLVCharacteristic, Equatable, Codable { @@ -18,7 +18,7 @@ public struct LockInformationCharacteristic: TLVCharacteristic, Equatable, Codab public static let properties: Bluetooth.BitMaskOptionSet = [.read] /// Lock identifier - public let identifier: UUID + public let id: UUID /// Firmware build number public let buildVersion: LockBuildVersion @@ -32,13 +32,13 @@ public struct LockInformationCharacteristic: TLVCharacteristic, Equatable, Codab /// Supported lock actions public let unlockActions: BitMaskOptionSet - public init(identifier: UUID, + public init(id: UUID, buildVersion: LockBuildVersion = .current, version: LockVersion = .current, status: LockStatus, unlockActions: BitMaskOptionSet = [.default]) { - self.identifier = identifier + self.id = id self.buildVersion = buildVersion self.version = version self.status = status diff --git a/Sources/CoreLock/RemoveKeyCharacteristic.swift b/Sources/CoreLock/Bluetooth/RemoveKeyCharacteristic.swift similarity index 90% rename from Sources/CoreLock/RemoveKeyCharacteristic.swift rename to Sources/CoreLock/Bluetooth/RemoveKeyCharacteristic.swift index 3ca0f5b9..02b83d07 100644 --- a/Sources/CoreLock/RemoveKeyCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/RemoveKeyCharacteristic.swift @@ -7,6 +7,7 @@ import Foundation import Bluetooth +import GATT /// Remove the specified key. public struct RemoveKeyCharacteristic: TLVCharacteristic, Codable, Equatable { @@ -18,7 +19,7 @@ public struct RemoveKeyCharacteristic: TLVCharacteristic, Codable, Equatable { public static let properties: Bluetooth.BitMaskOptionSet = [.write] /// Identifier of key making request. - public let identifier: UUID + public let id: UUID /// Key to remove. public let key: UUID @@ -29,12 +30,12 @@ public struct RemoveKeyCharacteristic: TLVCharacteristic, Codable, Equatable { /// HMAC of key and nonce, and HMAC message public let authentication: Authentication - public init(identifier: UUID, + public init(id: UUID, key: UUID, type: KeyType, authentication: Authentication) { - self.identifier = identifier + self.id = id self.key = key self.type = type self.authentication = authentication diff --git a/Sources/CoreLock/SetupCharacteristic.swift b/Sources/CoreLock/Bluetooth/SetupCharacteristic.swift similarity index 87% rename from Sources/CoreLock/SetupCharacteristic.swift rename to Sources/CoreLock/Bluetooth/SetupCharacteristic.swift index f7e94337..603a340c 100644 --- a/Sources/CoreLock/SetupCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/SetupCharacteristic.swift @@ -7,6 +7,7 @@ import Foundation import Bluetooth +import GATT /// Used for initial lock setup. public struct SetupCharacteristic: TLVCharacteristic, Equatable { @@ -52,15 +53,15 @@ extension SetupCharacteristic: Codable { public struct SetupRequest: Equatable, Codable { /// Key identifier - public let identifier: UUID + public let id: UUID /// Key secret public let secret: KeyData - public init(identifier: UUID = UUID(), + public init(id: UUID = UUID(), secret: KeyData = KeyData()) { - self.identifier = identifier + self.id = id self.secret = secret } } @@ -70,9 +71,11 @@ public extension Key { /// Initialize a new owner key from a setup request. init(setup: SetupRequest) { - self.init(identifier: setup.identifier, - name: "Owner", - created: Date(), - permission: .owner) + self.init( + id: setup.id, + name: "Owner", + created: Date(), + permission: .owner + ) } } diff --git a/Sources/CoreLock/UnlockCharacteristic.swift b/Sources/CoreLock/Bluetooth/UnlockCharacteristic.swift similarity index 90% rename from Sources/CoreLock/UnlockCharacteristic.swift rename to Sources/CoreLock/Bluetooth/UnlockCharacteristic.swift index 0847fe9e..e85adbbc 100644 --- a/Sources/CoreLock/UnlockCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/UnlockCharacteristic.swift @@ -7,6 +7,7 @@ import Foundation import Bluetooth +import GATT /// Used to unlock door. public struct UnlockCharacteristic: TLVCharacteristic, Codable, Equatable { @@ -18,7 +19,7 @@ public struct UnlockCharacteristic: TLVCharacteristic, Codable, Equatable { public static let properties: Bluetooth.BitMaskOptionSet = [.write] /// Identifier of key making request. - public let identifier: UUID + public let id: UUID /// Unlock action. public let action: UnlockAction @@ -26,11 +27,11 @@ public struct UnlockCharacteristic: TLVCharacteristic, Codable, Equatable { /// HMAC of key and nonce, and HMAC message public let authentication: Authentication - public init(identifier: UUID, + public init(id: UUID, action: UnlockAction = .default, authentication: Authentication) { - self.identifier = identifier + self.id = id self.action = action self.authentication = authentication } diff --git a/Sources/CoreLock/Crypto.swift b/Sources/CoreLock/Crypto.swift deleted file mode 100644 index bc8010ca..00000000 --- a/Sources/CoreLock/Crypto.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// Crypto.swift -// Lock -// -// Created by Alsey Coleman Miller on 4/17/16. -// Copyright © 2016 ColemanCDA. All rights reserved. -// - -import Foundation -import CryptoSwift -import TLVCoding - -/// Generate random data with the specified size. -internal func random(_ size: Int) -> Data { - - let bytes = AES.randomIV(size) - - return Data(bytes) -} - -internal let HMACSize = 64 - -/// Performs HMAC with the specified key and message. -@inline(__always) -internal func HMAC(key: KeyData, message: AuthenticationMessage) -> AuthenticationData { - - let encoder = TLVEncoder.lock - let messageData = try! encoder.encode(message) - - let hmac = try! CryptoSwift.HMAC(key: key.data.bytes, variant: .sha512).authenticate(messageData.bytes) - - assert(hmac.count == HMACSize) - - return AuthenticationData(data: Data(hmac))! -} - -internal let IVSize = AES.blockSize - -/// Encrypt data -internal func encrypt(key: Data, data: Data) throws -> (encrypted: Data, iv: InitializationVector) { - - let iv = InitializationVector() - let crypto = try AES(key: key, iv: iv) - let encryptedData = try crypto.encrypt(data.bytes) - return (Data(encryptedData), iv) -} - -/// Decrypt data -internal func decrypt(key: Data, iv: InitializationVector, data: Data) throws -> Data { - - assert(iv.data.count == IVSize) - - let crypto = try AES(key: key, iv: iv) - let byteValue = try crypto.decrypt(data.bytes) - return Data(byteValue) -} - -internal extension AES { - - convenience init(key: Data, iv: InitializationVector) throws { - try self.init(key: Array(key), blockMode: CBC(iv: Array(iv.data)), padding: .pkcs7) - } -} diff --git a/Sources/CoreLock/Crypto/Authentication.swift b/Sources/CoreLock/Crypto/Authentication.swift new file mode 100644 index 00000000..dafed141 --- /dev/null +++ b/Sources/CoreLock/Crypto/Authentication.swift @@ -0,0 +1,57 @@ +// +// AuthenticationMessage.swift +// CoreLock +// +// Created by Alsey Coleman Miller on 8/11/18. +// + +import Foundation + +public struct Authentication: Equatable, Codable { + + public let message: AuthenticationMessage + + public let signedData: AuthenticationData + + public init(key: KeyData, + message: AuthenticationMessage) { + + self.message = message + self.signedData = AuthenticationData(key: key, message: message) + } + + public func isAuthenticated(using key: KeyData) -> Bool { + return signedData.isAuthenticated(message, using: key) + } +} + +/// HMAC Message +public struct AuthenticationMessage: Equatable, Codable { + + public let date: Date + + public let nonce: Nonce + + public let digest: Digest + + public init( + date: Date = Date(), + nonce: Nonce = Nonce(), + digest: Digest + ) { + self.date = date + self.nonce = nonce + self.digest = digest + } +} + +public extension AuthenticationData { + + init(key: KeyData, message: AuthenticationMessage) { + self = authenticationCode(for: message, using: key) + } + + func isAuthenticated(_ message: AuthenticationMessage, using key: KeyData) -> Bool { + return data == AuthenticationData(key: key, message: message).data + } +} diff --git a/Sources/CoreLock/AuthenticationError.swift b/Sources/CoreLock/Crypto/AuthenticationError.swift similarity index 100% rename from Sources/CoreLock/AuthenticationError.swift rename to Sources/CoreLock/Crypto/AuthenticationError.swift diff --git a/Sources/CoreLock/Crypto/Crypto.swift b/Sources/CoreLock/Crypto/Crypto.swift new file mode 100644 index 00000000..26f6e919 --- /dev/null +++ b/Sources/CoreLock/Crypto/Crypto.swift @@ -0,0 +1,113 @@ +// +// Crypto.swift +// Lock +// +// Created by Alsey Coleman Miller on 4/17/16. +// Copyright © 2016 ColemanCDA. All rights reserved. +// + +import Foundation +import TLVCoding +#if canImport(CryptoKit) +import CryptoKit +#elseif canImport(Crypto) +import Crypto +#endif + +#if canImport(CryptoKit) +typealias HMAC = CryptoKit.HMAC +#elseif canImport(Crypto) +typealias HMAC = Crypto.HMAC +#endif + +/// Performs HMAC with the specified key and message. +internal func authenticationCode(for message: AuthenticationMessage, using key: KeyData) -> AuthenticationData { + let encoder = TLVEncoder.lock + let messageData = try! encoder.encode(message) + let authenticationCode = HMAC.authenticationCode(for: messageData, using: SymmetricKey(key)) + return AuthenticationData(authenticationCode) +} + +/// Encrypt data +internal func encrypt(_ data: Data, using key: KeyData) throws -> Data { + do { + let authenticationData = Data(SHA512.hash(data: data)) + let sealed = try ChaChaPoly.seal(data, using: SymmetricKey(key), nonce: .init(), authenticating: authenticationData) + return sealed.combined + } catch { + throw AuthenticationError.encryptionError(error) + } +} + +/// Decrypt data +internal func decrypt(_ data: Data, using key: KeyData, authentication: AuthenticationMessage) throws -> Data { + do { + let authenticationData = Data(SHA512.hash(data: data)) + let sealed = try ChaChaPoly.SealedBox(combined: data) + let decrypted = try ChaChaPoly.open(sealed, using: SymmetricKey(key), authenticating: authenticationData) + return decrypted + } catch { + throw AuthenticationError.decryptionError(error) + } +} + +// MARK: - Extensions + +public extension KeyData { + + internal static var keySize: SymmetricKeySize { .bits256 } + + static var length: Int { Self.keySize.bitCount / 8 } + + /// Initializes a `Key` with a random value. + init() { + let key = SymmetricKey(size: .bits256) + self.data = key.withUnsafeBytes { Data($0) } + assert(data.count == Self.length) + } +} + +public extension Nonce { + + static var length: Int { 96 / 8 } + + init() { + let nonce = ChaChaPoly.Nonce() + self.data = Data(nonce) + assert(data.count == Self.length) + } +} + +public extension Digest { + + static var length: Int { SHA512.Digest.byteCount } + + init(hash data: Data) { + let hash = SHA512.hash(data: data) + self.data = Data(hash) + } +} + +public extension AuthenticationData { + + static var length: Int { 32 } +} + +internal extension SymmetricKey { + init(_ key: KeyData) { + self.init(data: key.data) + } +} + +internal extension ChaChaPoly.Nonce { + init(_ nonce: CoreLock.Nonce) { + try! self.init(data: nonce.data) + } +} + +internal extension AuthenticationData { + init(_ code: HMAC.MAC) { + self.data = Data(code) + assert(data.count == Self.length) + } +} diff --git a/Sources/CoreLock/Crypto/EncryptedData.swift b/Sources/CoreLock/Crypto/EncryptedData.swift new file mode 100644 index 00000000..9b05a70a --- /dev/null +++ b/Sources/CoreLock/Crypto/EncryptedData.swift @@ -0,0 +1,37 @@ +// +// EncryptedData.swift +// CoreLock +// +// Created by Alsey Coleman Miller on 8/11/18. +// + +import Foundation + +public struct EncryptedData: Equatable, Codable { + + /// HMAC signature, signed by secret. + public let authentication: Authentication + + /// Encrypted data + public let encryptedData: Data +} + +public extension EncryptedData { + + init(encrypt data: Data, with key: KeyData) throws { + let digest = Digest(hash: data) + let message = AuthenticationMessage(digest: digest) + let encryptedData = try encrypt(data, using: key, nonce: message.nonce) + let authentication = Authentication(key: key, message: message) + self.authentication = authentication + self.encryptedData = encryptedData + } + + func decrypt(with key: KeyData) throws -> Data { + // validate HMAC + guard authentication.isAuthenticated(using: key) + else { throw AuthenticationError.invalidAuthentication } + // attempt to decrypt + return try CoreLock.decrypt(encryptedData, using: key) + } +} diff --git a/Sources/CoreLock/SecureData.swift b/Sources/CoreLock/Crypto/SecureData.swift similarity index 73% rename from Sources/CoreLock/SecureData.swift rename to Sources/CoreLock/Crypto/SecureData.swift index 1fa156be..8cd0a907 100644 --- a/Sources/CoreLock/SecureData.swift +++ b/Sources/CoreLock/Crypto/SecureData.swift @@ -8,7 +8,7 @@ import Foundation -/// Secure Data Protocol. +/// Secure Data Protocol public protocol SecureData: Hashable { /// The data length. @@ -19,15 +19,11 @@ public protocol SecureData: Hashable { /// Initialize with data. init?(data: Data) - - /// Initialize with random value. - init() } public extension SecureData where Self: Decodable { init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() let data = try container.decode(Data.self) guard let value = Self(data: data) else { @@ -40,7 +36,6 @@ public extension SecureData where Self: Decodable { public extension SecureData where Self: Encodable { func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() try container.encode(data) } @@ -48,63 +43,47 @@ public extension SecureData where Self: Encodable { /// A lock's key used for unlocking and actions. public struct KeyData: SecureData, Codable { - - public static let length = 256 / 8 // 32 - + public let data: Data public init?(data: Data) { - guard data.count == type(of: self).length else { return nil } - self.data = data } - - /// Initializes a `Key` with a random value. - public init() { - - self.data = random(type(of: self).length) - } } /// Cryptographic nonce public struct Nonce: SecureData, Codable { - - public static let length = 16 - + public let data: Data public init?(data: Data) { - guard data.count == type(of: self).length else { return nil } - self.data = data } - - public init() { - - self.data = random(type(of: self).length) - } } -public struct InitializationVector: SecureData, Codable { - - public static let length = IVSize - +public struct Digest: SecureData, Codable { + public let data: Data public init?(data: Data) { - guard data.count == type(of: self).length else { return nil } - self.data = data } - - public init() { +} + +/// HMAC data +public struct AuthenticationData: SecureData, Codable { - self.data = random(type(of: self).length) + public let data: Data + + public init?(data: Data) { + guard data.count == type(of: self).length + else { return nil } + self.data = data } } diff --git a/Sources/CoreLock/EncryptedData.swift b/Sources/CoreLock/EncryptedData.swift deleted file mode 100644 index 770586c9..00000000 --- a/Sources/CoreLock/EncryptedData.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// EncryptedData.swift -// CoreLock -// -// Created by Alsey Coleman Miller on 8/11/18. -// - -import Foundation - -public struct EncryptedData: Equatable, Codable { - - /// HMAC signature, signed by secret. - public let authentication: Authentication - - /// Crypto IV - public let initializationVector: InitializationVector - - /// Encrypted data - public let encryptedData: Data -} - -public extension EncryptedData { - - init(encrypt data: Data, with key: KeyData) throws { - - do { - let (encryptedData, iv) = try CoreLock.encrypt(key: key.data, data: data) - self.authentication = Authentication(key: key) - self.initializationVector = iv - self.encryptedData = encryptedData - } - - catch { throw AuthenticationError.encryptionError(error) } - } - - func decrypt(with key: KeyData) throws -> Data { - - guard authentication.isAuthenticated(with: key) - else { throw AuthenticationError.invalidAuthentication } - - // 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 3c44c190..44ef647f 100644 --- a/Sources/CoreLock/Event.swift +++ b/Sources/CoreLock/Event.swift @@ -19,18 +19,18 @@ public enum LockEvent: Equatable { public extension LockEvent { - var identifier: UUID { + var id: UUID { switch self { case let .setup(event): - return event.identifier + return event.id case let .unlock(event): - return event.identifier + return event.id case let .createNewKey(event): - return event.identifier + return event.id case let .confirmNewKey(event): - return event.identifier + return event.id case let .removeKey(event): - return event.identifier + return event.id } } @@ -168,17 +168,17 @@ public extension LockEvent { struct Setup: Codable, Equatable { - public let identifier: UUID + public let id: UUID public let date: Date public let key: UUID - public init(identifier: UUID = UUID(), + public init(id: UUID = UUID(), date: Date = Date(), key: UUID) { - self.identifier = identifier + self.id = id self.date = date self.key = key } @@ -186,7 +186,7 @@ public extension LockEvent { struct Unlock: Codable, Equatable { - public let identifier: UUID + public let id: UUID public let date: Date @@ -194,12 +194,12 @@ public extension LockEvent { public let action: UnlockAction - public init(identifier: UUID = UUID(), + public init(id: UUID = UUID(), date: Date = Date(), key: UUID, action: UnlockAction = .default) { - self.identifier = identifier + self.id = id self.date = date self.key = key self.action = action @@ -208,7 +208,7 @@ public extension LockEvent { struct CreateNewKey: Codable, Equatable { - public let identifier: UUID + public let id: UUID public let date: Date @@ -216,11 +216,11 @@ public extension LockEvent { public let newKey: UUID - public init(identifier: UUID = UUID(), + public init(id: UUID = UUID(), date: Date = Date(), key: UUID, newKey: UUID) { - self.identifier = identifier + self.id = id self.date = date self.key = key self.newKey = newKey @@ -229,7 +229,7 @@ public extension LockEvent { struct ConfirmNewKey: Codable, Equatable { - public let identifier: UUID + public let id: UUID public let date: Date @@ -239,11 +239,11 @@ public extension LockEvent { /// The newly created key. public let key: UUID - public init(identifier: UUID = UUID(), + public init(id: UUID = UUID(), date: Date = Date(), newKey: UUID, key: UUID) { - self.identifier = identifier + self.id = id self.date = date self.newKey = newKey self.key = key @@ -252,7 +252,7 @@ public extension LockEvent { struct RemoveKey: Codable, Equatable { - public let identifier: UUID + public let id: UUID public let date: Date @@ -262,12 +262,12 @@ public extension LockEvent { public let type: KeyType - public init(identifier: UUID = UUID(), + public init(id: UUID = UUID(), date: Date = Date(), key: UUID, removedKey: UUID, type: KeyType) { - self.identifier = identifier + self.id = id self.date = date self.key = key self.removedKey = removedKey diff --git a/Sources/CoreLock/Bool.swift b/Sources/CoreLock/Extensions/Bool.swift similarity index 100% rename from Sources/CoreLock/Bool.swift rename to Sources/CoreLock/Extensions/Bool.swift diff --git a/Sources/CoreLock/Integer.swift b/Sources/CoreLock/Extensions/Integer.swift similarity index 100% rename from Sources/CoreLock/Integer.swift rename to Sources/CoreLock/Extensions/Integer.swift diff --git a/Sources/CoreLock/Key.swift b/Sources/CoreLock/Key.swift index e260991e..909023cb 100644 --- a/Sources/CoreLock/Key.swift +++ b/Sources/CoreLock/Key.swift @@ -9,10 +9,10 @@ import Foundation import TLVCoding /// A smart lock key. -public struct Key: Codable, Equatable, Hashable { +public struct Key: Identifiable, Codable, Equatable, Hashable { /// The unique identifier of the key. - public let identifier: UUID + public let id: UUID /// The name of the key. public let name: String @@ -23,12 +23,12 @@ public struct Key: Codable, Equatable, Hashable { /// Key's permissions. public let permission: Permission - public init(identifier: UUID = UUID(), + public init(id: UUID = UUID(), name: String = "", created: Date = Date(), permission: Permission) { - self.identifier = identifier + self.id = id self.name = name self.created = created self.permission = permission diff --git a/Sources/CoreLock/LockAuthorizationStore.swift b/Sources/CoreLock/LockAuthorizationStore.swift index 29aeedfb..7eb5a98b 100644 --- a/Sources/CoreLock/LockAuthorizationStore.swift +++ b/Sources/CoreLock/LockAuthorizationStore.swift @@ -14,15 +14,15 @@ public protocol LockAuthorizationStore: class { func add(_ key: Key, secret: KeyData) throws - func key(for identifier: UUID) throws -> (key: Key, secret: KeyData)? + func key(for id: UUID) throws -> (key: Key, secret: KeyData)? func add(_ key: NewKey, secret: KeyData) throws - func newKey(for identifier: UUID) throws -> (newKey: NewKey, secret: KeyData)? + func newKey(for id: UUID) throws -> (newKey: NewKey, secret: KeyData)? - func removeKey(_ identifier: UUID) throws + func removeKey(_ id: UUID) throws - func removeNewKey(_ identifier: UUID) throws + func removeNewKey(_ id: UUID) throws func removeAll() throws @@ -49,9 +49,9 @@ public final class InMemoryLockAuthorization: LockAuthorizationStore { keys.append(KeyEntry(key: key, secret: secret)) } - public func key(for identifier: UUID) throws -> (key: Key, secret: KeyData)? { + public func key(for id: UUID) throws -> (key: Key, secret: KeyData)? { - guard let keyEntry = keys.first(where: { $0.key.identifier == identifier }) + guard let keyEntry = keys.first(where: { $0.key.id == id }) else { return nil } return (keyEntry.key, keyEntry.secret) @@ -62,22 +62,22 @@ public final class InMemoryLockAuthorization: LockAuthorizationStore { newKeys.append(NewKeyEntry(newKey: key, secret: secret)) } - public func newKey(for identifier: UUID) throws -> (newKey: NewKey, secret: KeyData)? { + public func newKey(for id: UUID) throws -> (newKey: NewKey, secret: KeyData)? { - guard let keyEntry = newKeys.first(where: { $0.newKey.identifier == identifier }) + guard let keyEntry = newKeys.first(where: { $0.newKey.id == id }) else { return nil } return (keyEntry.newKey, keyEntry.secret) } - public func removeKey(_ identifier: UUID) throws { + public func removeKey(_ id: UUID) throws { - keys.removeAll(where: { $0.key.identifier == identifier }) + keys.removeAll(where: { $0.key.id == id }) } - public func removeNewKey(_ identifier: UUID) throws { + public func removeNewKey(_ id: UUID) throws { - newKeys.removeAll(where: { $0.newKey.identifier == identifier }) + newKeys.removeAll(where: { $0.newKey.id == id }) } public func removeAll() throws { diff --git a/Sources/CoreLock/LockConfiguration.swift b/Sources/CoreLock/LockConfiguration.swift index c84052e7..1e9485b4 100644 --- a/Sources/CoreLock/LockConfiguration.swift +++ b/Sources/CoreLock/LockConfiguration.swift @@ -12,15 +12,15 @@ import Foundation public struct LockConfiguration: Codable, Equatable, Hashable { /// Lock identifier UUID - public let identifier: UUID + public let id: UUID /// Lock name public var name: String? - public init(identifier: UUID = UUID(), + public init(id: UUID = UUID(), name: String? = nil) { - self.identifier = identifier + self.id = id self.name = name } } diff --git a/Sources/CoreLock/LockHardware.swift b/Sources/CoreLock/LockHardware.swift index b253b40b..6a4be32a 100644 --- a/Sources/CoreLock/LockHardware.swift +++ b/Sources/CoreLock/LockHardware.swift @@ -37,19 +37,3 @@ public extension LockHardware { return .init(model: "", hardwareRevision: "", serialNumber: "") } } - -// MARK: - Darwin - -#if os(macOS) - - public extension LockHardware { - - static var mac: LockHardware { - - return LockHardware(model: .currentMac, - hardwareRevision: UIDevice.current.modelIdentifier, - serialNumber: UIDevice.current.serialNumber) - } - } - -#endif diff --git a/Sources/CoreLock/LockModel.swift b/Sources/CoreLock/LockModel.swift index 50a8f6c3..a8f11554 100644 --- a/Sources/CoreLock/LockModel.swift +++ b/Sources/CoreLock/LockModel.swift @@ -41,25 +41,12 @@ extension LockModel: ExpressibleByStringLiteral { public extension LockModel { static let orangePiOne: LockModel = "OrangePiOne" - static let orangePiZero: LockModel = "OrangePiZero" - + static let orangePiZero2: LockModel = "OrangePiZero2" static let raspberryPi3: LockModel = "RaspberryPi3" + static let raspberryPi4: LockModel = "RaspberryPi4" } -// MARK: - Darwin - -#if os(macOS) - -public extension LockModel { - - static var currentMac: LockModel { - return LockModel(rawValue: UIDevice.current.model) - } -} - -#endif - // MARK: - Codable extension LockModel: Codable { diff --git a/Sources/CoreLock/CreateNewKeyRequest.swift b/Sources/CoreLock/Networking/CreateNewKeyRequest.swift similarity index 99% rename from Sources/CoreLock/CreateNewKeyRequest.swift rename to Sources/CoreLock/Networking/CreateNewKeyRequest.swift index cf7ba36e..e7f69e04 100644 --- a/Sources/CoreLock/CreateNewKeyRequest.swift +++ b/Sources/CoreLock/Networking/CreateNewKeyRequest.swift @@ -11,7 +11,7 @@ import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif - +/* public struct CreateNewKeyNetServiceRequest: Equatable { /// Lock server @@ -89,3 +89,4 @@ public extension LockNetService.Client { else { throw LockNetService.Error.statusCode(httpResponse.statusCode) } } } +*/ diff --git a/Sources/CoreLock/DeleteKeyRequest.swift b/Sources/CoreLock/Networking/DeleteKeyRequest.swift similarity index 98% rename from Sources/CoreLock/DeleteKeyRequest.swift rename to Sources/CoreLock/Networking/DeleteKeyRequest.swift index 8f60aff0..9ced172c 100644 --- a/Sources/CoreLock/DeleteKeyRequest.swift +++ b/Sources/CoreLock/Networking/DeleteKeyRequest.swift @@ -11,7 +11,7 @@ import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif - +/* /// Lock Software Update HTTP Request public struct DeleteKeyRequest: Equatable { @@ -50,7 +50,7 @@ public extension DeleteKeyRequest { public extension LockNetService.Client { /// Remove the specified key. - func removeKey(_ identifier: UUID, + func removeKey(_ id: UUID, type: KeyType = .key, for server: LockNetService, with key: KeyCredentials, @@ -71,3 +71,4 @@ public extension LockNetService.Client { else { throw LockNetService.Error.statusCode(httpResponse.statusCode) } } } +*/ diff --git a/Sources/CoreLock/EventsRequest.swift b/Sources/CoreLock/Networking/EventsRequest.swift similarity index 99% rename from Sources/CoreLock/EventsRequest.swift rename to Sources/CoreLock/Networking/EventsRequest.swift index c93de29d..0de60375 100644 --- a/Sources/CoreLock/EventsRequest.swift +++ b/Sources/CoreLock/Networking/EventsRequest.swift @@ -11,7 +11,7 @@ import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif - +/* public struct EventsNetServiceRequest: Equatable { /// Lock server @@ -147,3 +147,4 @@ public extension LockNetService.Client { return keys } } +*/ diff --git a/Sources/CoreLock/EventsResponse.swift b/Sources/CoreLock/Networking/EventsResponse.swift similarity index 99% rename from Sources/CoreLock/EventsResponse.swift rename to Sources/CoreLock/Networking/EventsResponse.swift index 3213698c..a53b6814 100644 --- a/Sources/CoreLock/EventsResponse.swift +++ b/Sources/CoreLock/Networking/EventsResponse.swift @@ -7,7 +7,7 @@ // import Foundation - +/* public struct EventsResponse: Equatable { public let encryptedData: LockNetService.EncryptedData @@ -45,3 +45,4 @@ public extension EventsResponse { return try decoder.decode(EventsList.self, from: data) } } +*/ diff --git a/Sources/CoreLock/KeysRequest.swift b/Sources/CoreLock/Networking/KeysRequest.swift similarity index 99% rename from Sources/CoreLock/KeysRequest.swift rename to Sources/CoreLock/Networking/KeysRequest.swift index 61237d80..aff95184 100644 --- a/Sources/CoreLock/KeysRequest.swift +++ b/Sources/CoreLock/Networking/KeysRequest.swift @@ -7,12 +7,11 @@ // import Foundation -import CryptoSwift #if canImport(FoundationNetworking) import FoundationNetworking #endif - +/* public struct KeysNetServiceRequest: Equatable { /// Lock server @@ -68,3 +67,4 @@ public extension LockNetService.Client { return keys } } +*/ diff --git a/Sources/CoreLock/KeysResponse.swift b/Sources/CoreLock/Networking/KeysResponse.swift similarity index 99% rename from Sources/CoreLock/KeysResponse.swift rename to Sources/CoreLock/Networking/KeysResponse.swift index 915fa539..27814a6a 100644 --- a/Sources/CoreLock/KeysResponse.swift +++ b/Sources/CoreLock/Networking/KeysResponse.swift @@ -7,7 +7,7 @@ // import Foundation - +/* public struct KeysResponse: Equatable { public let encryptedData: LockNetService.EncryptedData @@ -45,3 +45,4 @@ public extension KeysResponse { return try decoder.decode(KeysList.self, from: data) } } +*/ diff --git a/Sources/CoreLock/LockInformationRequest.swift b/Sources/CoreLock/Networking/LockInformationRequest.swift similarity index 99% rename from Sources/CoreLock/LockInformationRequest.swift rename to Sources/CoreLock/Networking/LockInformationRequest.swift index f529ab22..c36d7dc7 100644 --- a/Sources/CoreLock/LockInformationRequest.swift +++ b/Sources/CoreLock/Networking/LockInformationRequest.swift @@ -11,7 +11,7 @@ import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif - +/* /// Lock Information Web Request public struct LockInformationNetServiceRequest: Equatable { @@ -51,3 +51,4 @@ public extension LockNetService.Client { return response } } +*/ diff --git a/Sources/CoreLock/LockInformationResponse.swift b/Sources/CoreLock/Networking/LockInformationResponse.swift similarity index 90% rename from Sources/CoreLock/LockInformationResponse.swift rename to Sources/CoreLock/Networking/LockInformationResponse.swift index 275d0e9b..c5181283 100644 --- a/Sources/CoreLock/LockInformationResponse.swift +++ b/Sources/CoreLock/Networking/LockInformationResponse.swift @@ -7,13 +7,13 @@ // import Foundation - +/* public extension LockNetService { struct LockInformation: Equatable, Codable { /// Lock identifier - public let identifier: UUID + public let id: UUID /// Firmware build number public let buildVersion: LockBuildVersion @@ -27,13 +27,13 @@ public extension LockNetService { /// Supported lock actions public let unlockActions: Set - public init(identifier: UUID, + public init(id: UUID, buildVersion: LockBuildVersion = .current, version: LockVersion = .current, status: LockStatus, unlockActions: Set = [.default]) { - self.identifier = identifier + self.id = id self.buildVersion = buildVersion self.version = version self.status = status @@ -41,3 +41,4 @@ public extension LockNetService { } } } +*/ diff --git a/Sources/CoreLock/LockNetService.swift b/Sources/CoreLock/Networking/LockNetService.swift similarity index 97% rename from Sources/CoreLock/LockNetService.swift rename to Sources/CoreLock/Networking/LockNetService.swift index c1270f38..e0b4bdbd 100644 --- a/Sources/CoreLock/LockNetService.swift +++ b/Sources/CoreLock/Networking/LockNetService.swift @@ -4,7 +4,7 @@ // // Created by Alsey Coleman Miller on 10/16/19. // - +/* import Foundation import Bonjour @@ -14,19 +14,19 @@ import FoundationNetworking public struct LockNetService: Equatable, Hashable { - public let identifier: UUID + public let id: UUID public let url: URL } internal extension LockNetService { - init(identifier: UUID, address: NetServiceAddress) { + init(id: UUID, address: NetServiceAddress) { guard let url = URL(string: "http://" + address.description) else { fatalError("Could not create URL from \(address)") } - self.identifier = identifier + self.id = id self.url = url } } @@ -221,3 +221,4 @@ extension LockNetService.EncryptedData { catch { throw AuthenticationError.decryptionError(error) } } } +*/ diff --git a/Sources/CoreLock/URL.swift b/Sources/CoreLock/Networking/URL.swift similarity index 100% rename from Sources/CoreLock/URL.swift rename to Sources/CoreLock/Networking/URL.swift diff --git a/Sources/CoreLock/URLSession.swift b/Sources/CoreLock/Networking/URLSession.swift similarity index 100% rename from Sources/CoreLock/URLSession.swift rename to Sources/CoreLock/Networking/URLSession.swift diff --git a/Sources/CoreLock/UpdateRequest.swift b/Sources/CoreLock/Networking/UpdateRequest.swift similarity index 99% rename from Sources/CoreLock/UpdateRequest.swift rename to Sources/CoreLock/Networking/UpdateRequest.swift index 8c3a4707..ef6787d6 100644 --- a/Sources/CoreLock/UpdateRequest.swift +++ b/Sources/CoreLock/Networking/UpdateRequest.swift @@ -10,7 +10,7 @@ import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif - +/* /// Lock Software Update HTTP Request public struct UpdateNetServiceRequest: Equatable { @@ -58,3 +58,4 @@ public extension LockNetService.Client { else { throw LockNetService.Error.statusCode(httpResponse.statusCode) } } } +*/ diff --git a/Sources/CoreLock/NewKey.swift b/Sources/CoreLock/NewKey.swift index 8077da97..98f28109 100644 --- a/Sources/CoreLock/NewKey.swift +++ b/Sources/CoreLock/NewKey.swift @@ -11,7 +11,7 @@ import Foundation public struct NewKey: Codable, Equatable, Hashable { /// The unique identifier of the key. - public let identifier: UUID + public let id: UUID /// The name of the key. public let name: String @@ -25,13 +25,13 @@ public struct NewKey: Codable, Equatable, Hashable { /// Expiration date for new key invitation. public let expiration: Date - public init(identifier: UUID = UUID(), + public init(id: UUID = UUID(), name: String = "", permission: Permission = .anytime, created: Date = Date(), expiration: Date = Date().addingTimeInterval(60 * 60 * 24)) { - self.identifier = identifier + self.id = id self.name = name self.permission = permission self.created = created diff --git a/Sources/CoreLock/Permission.swift b/Sources/CoreLock/Permission.swift index 335a4aef..888c0b10 100644 --- a/Sources/CoreLock/Permission.swift +++ b/Sources/CoreLock/Permission.swift @@ -61,10 +61,10 @@ public extension Permission { /// A Key's permission level. public enum PermissionType: UInt8, CaseIterable { - case owner - case admin - case anytime - case scheduled + case owner = 0x00 + case admin = 0x01 + case anytime = 0x02 + case scheduled = 0x03 } internal extension PermissionType { diff --git a/Sources/CoreLock/TLV.swift b/Sources/CoreLock/TLV.swift index 58e16ed7..5b2be5d0 100644 --- a/Sources/CoreLock/TLV.swift +++ b/Sources/CoreLock/TLV.swift @@ -12,8 +12,8 @@ internal extension TLVEncoder { static var lock: TLVEncoder { var encoder = TLVEncoder() - encoder.numericFormat = .littleEndian - encoder.uuidFormat = .bytes + encoder.numericFormatting = .littleEndian + encoder.uuidFormatting = .bytes return encoder } } @@ -22,8 +22,8 @@ internal extension TLVDecoder { static var lock: TLVDecoder { var decoder = TLVDecoder() - decoder.numericFormat = .littleEndian - decoder.uuidFormat = .bytes + decoder.numericFormatting = .littleEndian + decoder.uuidFormatting = .bytes return decoder } } diff --git a/Sources/CoreLock/UIDevice.swift b/Sources/CoreLock/UIDevice.swift deleted file mode 100644 index 9ba2f341..00000000 --- a/Sources/CoreLock/UIDevice.swift +++ /dev/null @@ -1,427 +0,0 @@ -// -// UIDevice.swift -// gattserver -// -// Created by Alsey Coleman Miller on 6/29/18. -// -// - -#if os(macOS) - import Darwin -#elseif os(Linux) - import Glibc -#endif - -import Foundation - -#if os(macOS) || os(Linux) - -/// Use a `UIDevice` object to get information about the device such as assigned name, device model, -/// and operating-system name and version. You also use the UIDevice instance to detect changes in -/// the device’s characteristics, such as physical orientation. -internal final class UIDevice { - - // MARK: - Getting the Shared Device Instance - - /// Returns an object representing the current device. - public static let current = UIDevice() - - private init() { } - - // MARK: - Determining the Available Features - - /// A Boolean value indicating whether multitasking is supported on the current device. - public var isMultitaskingSupported: Bool { - - return true - } - - // MARK: - Identifying the Device and Operating System - - /// The name identifying the device. - public var name: String { - - #if os(macOS) - return Mac.name - #else - return Host.current().name ?? "" // Use NSHost on other platforms - #endif - } - - public var serialNumber: String { - - #if os(macOS) - return Mac.serialNumber ?? "" - #else - return "" - #endif - } - - public var modelIdentifier: String { - - #if os(macOS) - return Mac.modelIdentifier ?? "" - #else - return "" - #endif - } - - /// The name of the operating system running on the device represented by the receiver. - public var systemName: String { - - #if os(macOS) - return "macOS" - #elseif os(Android) - return "Android" - #elseif os(Linux) - return "Linux" - #endif - } - - /// The current version of the operating system. - public var systemVersion: String { - - return ProcessInfo.processInfo.operatingSystemVersionString - } - - /// The model of the device. - public var model: String { - - #if os(macOS) - return Mac.model - #elseif os(Android) - return "Android" - #elseif os(Linux) - return "Linux" - #endif - } - - /// The model of the device as a localized string. - public var localizedModel: String { - - // just forward model string, no localization - @inline(__always) - get { return model } - } - - /// The style of interface to use on the current device. - public var userInterfaceIdiom: UIUserInterfaceIdiom { - - #if os(macOS) - return .pad - #elseif os(Android) - return .phone - #elseif os(Linux) - return .pad - #endif - } - - /// An alphanumeric string that uniquely identifies a device to the app’s vendor. - public lazy var identifierForVendor: UUID? = UUID() - - // MARK: - Getting the Device Orientation - - /// Returns the physical orientation of the device. - public var orientation: UIDeviceOrientation { - - // FIXME: Implement rotation - - #if os(macOS) - return .portrait - #elseif os(Android) - return Android.orientation - #elseif os(Linux) - return .portrait - #endif - } - - /// A Boolean value that indicates whether the receiver generates orientation notifications (true) or not (false). - public private(set) var isGeneratingDeviceOrientationNotifications: Bool = false - - /// Begins the generation of notifications of device orientation changes. - public func beginGeneratingDeviceOrientationNotifications() { - - isGeneratingDeviceOrientationNotifications = true - } - - /// Ends the generation of notifications of device orientation changes. - public func endGeneratingDeviceOrientationNotifications() { - - isGeneratingDeviceOrientationNotifications = false - } - - // MARK: - Getting the Device Battery State - - /// The battery charge level for the device. - /// - /// Battery level ranges from 0.0 (fully discharged) to 1.0 (100% charged). - /// Before accessing this property, ensure that battery monitoring is enabled. - /// If battery monitoring is not enabled, battery state is `unknown` - /// and the value of this property is –1.0. - public var batteryLevel: Float { - - #if os(macOS) - return Mac.batteryLevel - #elseif os(Linux) - return -1 - #endif - } - - /// A Boolean value indicating whether battery monitoring is enabled (true) or not (false). - public var isBatteryMonitoringEnabled: Bool = false - - /// The battery state for the device. - public var batteryState: UIDeviceBatteryState { - - #if os(macOS) - return Mac.batteryState - #elseif os(Linux) - return .unknown - #endif - } -} - -// MARK: - Supporting Types - -public enum UIDeviceOrientation: Int { - - case unknown - case portrait - case portraitUpsideDown - case landscapeLeft - case landscapeRight - case faceUp - case faceDown -} - -public enum UIUserInterfaceIdiom: Int { - - case phone - case pad -} - -public enum UIDeviceBatteryState: Int { - - /// The battery state for the device cannot be determined. - case unknown - - /// The device is not plugged into power; the battery is discharging. - case unplugged - - /// The device is plugged into power and the battery is less than 100% charged. - case charging - - /// The device is plugged into power and the battery is 100% charged. - case full -} - -// MARK: - Macintosh Information - -#if os(macOS) - - import SystemConfiguration - import IOKit.ps - - private extension UIDevice { - - /// Macintosh Device information - struct Mac { - - static func systemInformation(for name: String) -> String? { - - var size = 0 - - sysctlbyname(name, nil, &size, nil, 0) - - guard size > 0 else { return nil } - - let cString = UnsafeMutablePointer.allocate(capacity: size) - - defer { cString.deallocate() } - - sysctlbyname(name, cString, &size, nil, 0) - - return String(cString: cString) - } - - /// Get the computer name on a Macintosh. - static var name: String { - - return SCDynamicStoreCopyComputerName(nil, nil) as String? ?? "" - } - - static var modelIdentifier: String? { - let service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice")) - - var identifier: String? - - if let modelData = IORegistryEntryCreateCFProperty(service, "model" as CFString, kCFAllocatorDefault, 0).takeRetainedValue() as? Data { - - identifier = String(data: modelData, encoding: .utf8) - } - - IOObjectRelease(service) - return identifier - } - - /// Get the model name on a Macintosh. - static var model: String { - - guard let hardwareModel = systemInformation(for: "hw.model") - else { return "" } - - var family: Family? - - for model in Family.all { - - guard hardwareModel.hasPrefix(model.rawValue) - else { continue } - - family = model - break - } - - return family?.description ?? hardwareModel - } - - static var powerSources: [PowerSource] { - - let sourcesInfo = IOPSCopyPowerSourcesInfo().takeRetainedValue() - - if let list = IOPSCopyPowerSourcesList(sourcesInfo).takeRetainedValue() as? [[String: Any]] { - - return list.compactMap { PowerSource(info: $0) } - } - - return [] - } - - static var serialNumber: String? { - - let platformExpert = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice")) - - guard platformExpert > 0 - else { return nil } - - guard let registryEntry = IORegistryEntryCreateCFProperty(platformExpert, kIOPlatformSerialNumberKey as CFString, - kCFAllocatorDefault, 0).takeUnretainedValue() as? String - else { return nil } - - let serialNumber = registryEntry.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - - IOObjectRelease(platformExpert) - - return serialNumber - } - - static var batteryState: UIDeviceBatteryState { - - guard let powerSource = powerSources.first - else { return .unknown } - - switch powerSource.state { - - case kIOPSACPowerValue: - - // determine charging - if powerSource.currentCapacity == powerSource.maximumCapacity { - - return .full - - } else { - - return .charging - } - - case kIOPSBatteryPowerValue: - - return .unplugged - - default: return .unknown - } - } - - static var batteryLevel: Float { - - guard let powerSource = powerSources.first - else { return -1 } - - return powerSource.chargedPercentage - } - } - } - - extension UIDevice.Mac { - - enum Family: String, CustomStringConvertible { - - case iMac - case iMacPro - case MacBook - case MacBookPro - case MacBookAir - case Macmini - case MacPro - - static var all: [Family] = [iMacPro, iMac, MacBookPro, MacBookAir, MacBook, Macmini, MacPro] - - var description: String { - - switch self { - case .iMac: return "iMac" - case .iMacPro: return "iMac Pro" - case .MacBook: return "MacBook" - case .MacBookPro: return "MacBook Pro" - case .MacBookAir: return "MacBook Air" - case .Macmini: return "Mac Mini" - case .MacPro: return "Mac Pro" - } - } - } - - struct PowerSource { - - let identifier: Int - let serialNumber: String - let name: String - let maximumCapacity: Int - let currentCapacity: Int - let charging: Bool - let present: Bool - let state: String - let category: String - - var chargedPercentage: Float { - - return floor((Float(currentCapacity) / Float(maximumCapacity))) - } - - init?(info: [String: Any]) { - - guard let id = info[kIOPSPowerSourceIDKey] as? Int, - let serialNumber = info[kIOPSHardwareSerialNumberKey] as? String, - let name = info[kIOPSNameKey] as? String, - let maximumCapacity = info[kIOPSMaxCapacityKey] as? Int, - let currentCapacity = info[kIOPSCurrentCapacityKey] as? Int, - let charging = info[kIOPSIsChargingKey] as? Bool, - let present = info[kIOPSIsPresentKey] as? Bool, - let state = info[kIOPSPowerSourceStateKey] as? String, - let type = info[kIOPSTypeKey] as? String - else { return nil } - - self.identifier = id - self.serialNumber = serialNumber - self.name = name - self.maximumCapacity = maximumCapacity - self.currentCapacity = currentCapacity - self.charging = charging - self.present = present - self.state = state - self.category = type - } - } - } - -#endif - -#endif diff --git a/Sources/CoreLock/UnlockAction.swift b/Sources/CoreLock/UnlockAction.swift index e88d5144..1e025b0a 100644 --- a/Sources/CoreLock/UnlockAction.swift +++ b/Sources/CoreLock/UnlockAction.swift @@ -7,6 +7,7 @@ import Foundation import TLVCoding +import Bluetooth /// Unlock Action public enum UnlockAction: UInt8, BitMaskOption { diff --git a/Sources/CoreLock/Version.swift b/Sources/CoreLock/Version.swift index 8da964f7..ac34cb59 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: 1, minor: 0, patch: 1) } + static var current: LockVersion { return LockVersion(major: 1, minor: 0, patch: 0) } } // MARK: - RawRepresentable diff --git a/Sources/CoreLockWebServer/Permissions.swift b/Sources/CoreLockWebServer/Permissions.swift index e00df4f7..3bb8679a 100644 --- a/Sources/CoreLockWebServer/Permissions.swift +++ b/Sources/CoreLockWebServer/Permissions.swift @@ -140,7 +140,7 @@ internal extension LockWebServer { return .created } - private func deleteKey(_ identifier: UUID, type: KeyType, request: RouterRequest, response: RouterResponse) throws -> HTTPStatusCode { + private func deleteKey(_ id: UUID, type: KeyType, request: RouterRequest, response: RouterResponse) throws -> HTTPStatusCode { // authenticate guard let (key, _) = try authenticate(request: request) else { diff --git a/Sources/lockd/AuthorizationStoreFile.swift b/Sources/lockd/AuthorizationStoreFile.swift index 5d8cc205..e8bb9be5 100644 --- a/Sources/lockd/AuthorizationStoreFile.swift +++ b/Sources/lockd/AuthorizationStoreFile.swift @@ -53,7 +53,7 @@ public final class AuthorizationStoreFile: LockAuthorizationStore { try write { $0.keys.append(Database.KeyEntry(key: key, secret: secret)) } } - public func key(for identifier: UUID) throws -> (key: Key, secret: KeyData)? { + public func key(for id: UUID) throws -> (key: Key, secret: KeyData)? { guard let keyEntry = database.keys.first(where: { $0.key.identifier == identifier }) else { return nil } @@ -66,7 +66,7 @@ public final class AuthorizationStoreFile: LockAuthorizationStore { try write { $0.newKeys.append(Database.NewKeyEntry(newKey: key, secret: secret)) } } - public func newKey(for identifier: UUID) throws -> (newKey: NewKey, secret: KeyData)? { + public func newKey(for id: UUID) throws -> (newKey: NewKey, secret: KeyData)? { guard let keyEntry = database.newKeys.first(where: { $0.newKey.identifier == identifier }) else { return nil } @@ -74,12 +74,12 @@ public final class AuthorizationStoreFile: LockAuthorizationStore { return (keyEntry.newKey, keyEntry.secret) } - public func removeKey(_ identifier: UUID) throws { + public func removeKey(_ id: UUID) throws { try write { $0.keys.removeAll(where: { $0.key.identifier == identifier }) } } - public func removeNewKey(_ identifier: UUID) throws { + public func removeNewKey(_ id: UUID) throws { try write { $0.newKeys.removeAll(where: { $0.newKey.identifier == identifier }) } } diff --git a/Tests/CoreLockTests/GATTProfileTests.swift b/Tests/CoreLockTests/GATTProfileTests.swift index dbf6d872..318e37e3 100644 --- a/Tests/CoreLockTests/GATTProfileTests.swift +++ b/Tests/CoreLockTests/GATTProfileTests.swift @@ -12,17 +12,13 @@ import TLVCoding @testable import CoreLock final class GATTProfileTests: XCTestCase { - - static let allTests = [ - ("testInformation", testInformation), - ("testUnlock", testUnlock), - ("testSetup", testSetup) - ] func testInformation() { - let information = LockInformationCharacteristic(identifier: UUID(), - status: .setup) + let information = LockInformationCharacteristic( + id: UUID(), + status: .setup + ) guard let decoded = LockInformationCharacteristic(data: information.data) else { XCTFail("Could not parse bytes"); return } @@ -32,12 +28,12 @@ final class GATTProfileTests: XCTestCase { func testUnlock() { - let key = (identifier: UUID(), secret: KeyData()) + let key = (id: UUID(), secret: KeyData()) - let authentication = Authentication(key: key.secret) + let authentication = Authentication(key: key.secret, message: AuthenticationMessage(digest: Digest(hash: <#T##Data#>))) let characteristic = UnlockCharacteristic( - identifier: key.identifier, + id: key.id, authentication: authentication ) @@ -47,9 +43,9 @@ final class GATTProfileTests: XCTestCase { XCTAssertEqual(characteristic, decoded) XCTAssertEqual(try! TLVEncoder.lock.encode(decoded.authentication), try! TLVEncoder.lock.encode(authentication)) - XCTAssert(decoded.authentication.isAuthenticated(with: key.secret)) - XCTAssert(characteristic.authentication.isAuthenticated(with: key.secret)) - XCTAssertFalse(Authentication(key: KeyData()).isAuthenticated(with: key.secret)) + XCTAssert(decoded.authentication.isAuthenticated(using: key.secret)) + XCTAssert(characteristic.authentication.isAuthenticated(using: key.secret)) + XCTAssertFalse(Authentication(key: KeyData()).isAuthenticated(using: key.secret)) } func testSetup() { @@ -69,7 +65,7 @@ final class GATTProfileTests: XCTestCase { let decrypted = try! decoded.decrypt(with: deviceSharedSecret) XCTAssertEqual(request, decrypted) - XCTAssertEqual(request.identifier, decrypted.identifier) + XCTAssertEqual(request.id, decrypted.id) XCTAssertEqual(request.secret, decrypted.secret) } } diff --git a/iOS/LockKit/Controller/Extensions/Setup.swift b/iOS/LockKit/Controller/Extensions/Setup.swift index d3269562..4ac1b4ee 100644 --- a/iOS/LockKit/Controller/Extensions/Setup.swift +++ b/iOS/LockKit/Controller/Extensions/Setup.swift @@ -58,7 +58,7 @@ public extension ActivityIndicatorViewController where Self: UIViewController { public extension ActivityIndicatorViewController where Self: UIViewController { - func setup(lock identifier: UUID, secret: KeyData, name: String? = nil, scanDuration: TimeInterval = 2.0) { + func setup(lock id: UUID, secret: KeyData, name: String? = nil, scanDuration: TimeInterval = 2.0) { let name = name ?? R.string.localizable.newLockName() performActivity(queue: .bluetooth, { () -> Bool in diff --git a/iOS/LockKit/Controller/Extensions/Unlock.swift b/iOS/LockKit/Controller/Extensions/Unlock.swift index ca895f25..32f7600a 100644 --- a/iOS/LockKit/Controller/Extensions/Unlock.swift +++ b/iOS/LockKit/Controller/Extensions/Unlock.swift @@ -13,7 +13,7 @@ import Intents public extension ActivityIndicatorViewController where Self: UIViewController { - func unlock(lock identifier: UUID, action: UnlockAction = .default, scanDuration: TimeInterval = 2.0) { + func unlock(lock id: UUID, action: UnlockAction = .default, scanDuration: TimeInterval = 2.0) { log("Unlock \(identifier)") diff --git a/iOS/LockKit/Controller/Extensions/Update.swift b/iOS/LockKit/Controller/Extensions/Update.swift index 877b6ecc..5e7b2369 100644 --- a/iOS/LockKit/Controller/Extensions/Update.swift +++ b/iOS/LockKit/Controller/Extensions/Update.swift @@ -12,7 +12,7 @@ import CoreLock public extension ActivityIndicatorViewController where Self: UIViewController { - func update(lock identifier: UUID) { + func update(lock id: UUID) { guard let key = Store.shared.credentials(for: identifier) else { assertionFailure(); return } diff --git a/iOS/LockKit/Controller/LockPermissionsViewController.swift b/iOS/LockKit/Controller/LockPermissionsViewController.swift index c16d7c5d..a5f3ab80 100644 --- a/iOS/LockKit/Controller/LockPermissionsViewController.swift +++ b/iOS/LockKit/Controller/LockPermissionsViewController.swift @@ -17,7 +17,7 @@ public final class LockPermissionsViewController: UITableViewController { // MARK: - Properties - public var lockIdentifier: UUID! + public var lockid: UUID! public var completion: (() -> ())? @@ -318,7 +318,7 @@ extension LockPermissionsViewController { case key(Key) case newKey(NewKey) - var identifier: UUID { + var id: UUID { switch self { case let .key(value): return value.identifier case let .newKey(value): return value.identifier diff --git a/iOS/LockKit/Controller/LockViewController.swift b/iOS/LockKit/Controller/LockViewController.swift index bfb9cb17..941a6564 100644 --- a/iOS/LockKit/Controller/LockViewController.swift +++ b/iOS/LockKit/Controller/LockViewController.swift @@ -29,7 +29,7 @@ public final class LockViewController: UITableViewController { // MARK: - Properties - public var lockIdentifier: UUID! { + public var lockid: UUID! { didSet { if self.isViewLoaded { self.configureView() } } } @@ -229,7 +229,7 @@ extension LockViewController: ProgressHUDViewController { } public extension UIViewController { @discardableResult - func view(lock identifier: UUID) -> Bool { + func view(lock id: UUID) -> Bool { guard Store.shared[lock: identifier] != nil else { self.showErrorAlert(R.string.error.noKey()) diff --git a/iOS/LockKit/Controller/NewKeySelectPermissionViewController.swift b/iOS/LockKit/Controller/NewKeySelectPermissionViewController.swift index 2e6ace97..5d549507 100644 --- a/iOS/LockKit/Controller/NewKeySelectPermissionViewController.swift +++ b/iOS/LockKit/Controller/NewKeySelectPermissionViewController.swift @@ -19,7 +19,7 @@ public final class NewKeySelectPermissionViewController: UITableViewController, public var completion: (((invitation: NewKey.Invitation, sender: PopoverPresentingView)?) -> ())? - public var lockIdentifier: UUID! + public var lockid: UUID! public var progressHUD: JGProgressHUD? @@ -178,14 +178,14 @@ extension NewKeySelectPermissionViewController: ProgressHUDViewController { } public extension UIViewController { - func shareKey(lock identifier: UUID, completion: @escaping (((invitation: NewKey.Invitation, sender: PopoverPresentingView)?) -> ())) { + func shareKey(lock id: UUID, completion: @escaping (((invitation: NewKey.Invitation, sender: PopoverPresentingView)?) -> ())) { let newKeyViewController = NewKeySelectPermissionViewController.fromStoryboard(with: identifier, completion: completion) let navigationController = UINavigationController(rootViewController: newKeyViewController) self.present(navigationController, animated: true, completion: nil) } - func shareKey(lock identifier: UUID) { + func shareKey(lock id: UUID) { self.shareKey(lock: identifier) { [weak self] in guard let self = self else { return } diff --git a/iOS/LockKit/Controller/Protocols/NewKeyViewController.swift b/iOS/LockKit/Controller/Protocols/NewKeyViewController.swift index 38695f06..5059260b 100644 --- a/iOS/LockKit/Controller/Protocols/NewKeyViewController.swift +++ b/iOS/LockKit/Controller/Protocols/NewKeyViewController.swift @@ -12,7 +12,7 @@ import CoreLock public protocol NewKeyViewController: ActivityIndicatorViewController { - var lockIdentifier: UUID! { get } + var lockid: UUID! { get } var view: UIView! { get } diff --git a/iOS/LockKit/Model/Activity.swift b/iOS/LockKit/Model/Activity.swift index d4df133d..2eeeeafe 100644 --- a/iOS/LockKit/Model/Activity.swift +++ b/iOS/LockKit/Model/Activity.swift @@ -32,10 +32,10 @@ public final class LockActivityItem: NSObject { .markupAsPDF ] - public let identifier: UUID + public let id: UUID - public init(identifier: UUID) { - self.identifier = identifier + public init(id: UUID) { + self.id = id } // MARK: - Activity Values diff --git a/iOS/LockKit/Model/ApplicationData.swift b/iOS/LockKit/Model/ApplicationData.swift index d6dee429..49f83a89 100644 --- a/iOS/LockKit/Model/ApplicationData.swift +++ b/iOS/LockKit/Model/ApplicationData.swift @@ -13,7 +13,7 @@ import CoreLock public struct ApplicationData: Codable, Equatable { /// Identifier of app instance. - public let identifier: UUID + public let id: UUID /// Date application data was created. public let created: Date @@ -39,12 +39,12 @@ public struct ApplicationData: Codable, Equatable { self.locks = [:] } - public init(identifier: UUID, + public init(id: UUID, created: Date, updated: Date, locks: [UUID: LockCache]) { - self.identifier = identifier + self.id = id self.created = created self.updated = updated self.locks = locks @@ -57,12 +57,12 @@ public extension ApplicationData { return locks.values.map { $0.key } } - subscript (lock identifier: UUID) -> LockCache? { + subscript (lock id: UUID) -> LockCache? { get { return locks[identifier] } set { locks[identifier] = newValue } } - subscript (key identifier: UUID) -> Key? { + subscript (key id: UUID) -> Key? { return locks.values .lazy .map { $0.key } diff --git a/iOS/LockKit/Model/BeaconController.swift b/iOS/LockKit/Model/BeaconController.swift index 975b6579..6b04814a 100644 --- a/iOS/LockKit/Model/BeaconController.swift +++ b/iOS/LockKit/Model/BeaconController.swift @@ -87,7 +87,7 @@ public final class BeaconController { } @discardableResult - public func scanBeacon(for identifier: UUID) -> Bool { + public func scanBeacon(for id: UUID) -> Bool { guard let region = beacons[identifier]?.region else { return false } scanBeacons(in: region) @@ -379,7 +379,7 @@ internal extension CLLocationManager { startRangingBeacons(satisfying: .init(uuid: uuid)) } else { #if !targetEnvironment(macCatalyst) - startRangingBeacons(in: CLBeaconRegion(proximityUUID: uuid, identifier: uuid.uuidString)) + startRangingBeacons(in: CLBeaconRegion(proximityUUID: uuid, id: UUID.uuidString)) #endif } } @@ -389,7 +389,7 @@ internal extension CLLocationManager { stopRangingBeacons(satisfying: .init(uuid: uuid)) } else { #if !targetEnvironment(macCatalyst) - stopRangingBeacons(in: CLBeaconRegion(proximityUUID: uuid, identifier: uuid.uuidString)) + stopRangingBeacons(in: CLBeaconRegion(proximityUUID: uuid, id: UUID.uuidString)) #endif } } @@ -397,7 +397,7 @@ internal extension CLLocationManager { internal extension CLBeaconRegion { - convenience init(uuid identifier: UUID) { + convenience init(uuid id: UUID) { if #available(iOS 13.0, iOSApplicationExtension 13.0, *) { self.init(beaconIdentityConstraint: .init(uuid: identifier), identifier: identifier.uuidString) } else { diff --git a/iOS/LockKit/Model/CoreData/ContactManagedObject.swift b/iOS/LockKit/Model/CoreData/ContactManagedObject.swift index f5e4bd78..368be0f0 100644 --- a/iOS/LockKit/Model/CoreData/ContactManagedObject.swift +++ b/iOS/LockKit/Model/CoreData/ContactManagedObject.swift @@ -17,7 +17,7 @@ public final class ContactManagedObject: NSManagedObject { internal convenience init(identifier: String, context: NSManagedObjectContext) { self.init(context: context) - self.identifier = identifier + self.id = id } internal static func find(_ identifier: String, in context: NSManagedObjectContext) throws -> ContactManagedObject? { diff --git a/iOS/LockKit/Model/CoreData/EventManagedObject.swift b/iOS/LockKit/Model/CoreData/EventManagedObject.swift index 3880d306..41f162ff 100644 --- a/iOS/LockKit/Model/CoreData/EventManagedObject.swift +++ b/iOS/LockKit/Model/CoreData/EventManagedObject.swift @@ -30,7 +30,7 @@ public class EventManagedObject: NSManagedObject { } } - internal static func find(_ identifier: UUID, in context: NSManagedObjectContext) throws -> EventManagedObject? { + internal static func find(_ id: UUID, in context: NSManagedObjectContext) throws -> EventManagedObject? { try context.find(identifier: identifier as NSUUID, propertyName: #keyPath(EventManagedObject.identifier), diff --git a/iOS/LockKit/Model/CoreData/LockManagedObject.swift b/iOS/LockKit/Model/CoreData/LockManagedObject.swift index e8ddd0ed..f010b0ec 100644 --- a/iOS/LockKit/Model/CoreData/LockManagedObject.swift +++ b/iOS/LockKit/Model/CoreData/LockManagedObject.swift @@ -12,13 +12,13 @@ import CoreLock public final class LockManagedObject: NSManagedObject { - internal convenience init(identifier: UUID, + internal convenience init(id: UUID, name: String, information: LockCache.Information? = nil, context: NSManagedObjectContext) { self.init(context: context) - self.identifier = identifier + self.id = id self.name = name if let information = information { update(information: information, context: context) diff --git a/iOS/LockKit/Model/CoreData/ManagedObject.swift b/iOS/LockKit/Model/CoreData/ManagedObject.swift index b5a47d42..1a34f986 100644 --- a/iOS/LockKit/Model/CoreData/ManagedObject.swift +++ b/iOS/LockKit/Model/CoreData/ManagedObject.swift @@ -77,12 +77,12 @@ internal extension NSManagedObjectContext { public protocol IdentifiableManagedObject { - var identifier: UUID? { get } + var id: UUID? { get } } public extension NSManagedObjectContext { - func find(identifier: UUID, type: T.Type) throws -> T? where T: IdentifiableManagedObject, T: NSManagedObject { + func find(id: UUID, type: T.Type) throws -> T? where T: IdentifiableManagedObject, T: NSManagedObject { let fetchRequest = NSFetchRequest() fetchRequest.entity = T.entity() diff --git a/iOS/LockKit/Model/CoreData/RemoveKeyEventManagedObject.swift b/iOS/LockKit/Model/CoreData/RemoveKeyEventManagedObject.swift index 372b567b..df7b844a 100644 --- a/iOS/LockKit/Model/CoreData/RemoveKeyEventManagedObject.swift +++ b/iOS/LockKit/Model/CoreData/RemoveKeyEventManagedObject.swift @@ -87,7 +87,7 @@ public extension RemoveKeyEventManagedObject { public extension RemoveKeyEventManagedObject.RemovedKey { - var identifier: UUID? { + var id: UUID? { switch self { case let .key(managedObject): return managedObject.identifier case let .newKey(managedObject): return managedObject.identifier diff --git a/iOS/LockKit/Model/CoreSpotlight.swift b/iOS/LockKit/Model/CoreSpotlight.swift index a3d118ab..ed798f94 100644 --- a/iOS/LockKit/Model/CoreSpotlight.swift +++ b/iOS/LockKit/Model/CoreSpotlight.swift @@ -145,7 +145,7 @@ public extension CoreSpotlightSearchable { public struct SearchableLock: Equatable { - public let identifier: UUID + public let id: UUID public let cache: LockCache } diff --git a/iOS/LockKit/Model/Intent.swift b/iOS/LockKit/Model/Intent.swift index 3be2bb69..4008670d 100644 --- a/iOS/LockKit/Model/Intent.swift +++ b/iOS/LockKit/Model/Intent.swift @@ -17,13 +17,13 @@ import UIKit public extension UnlockIntent { @available(*, deprecated) - convenience init(lock identifier: UUID, name: String) { + convenience init(lock id: UUID, name: String) { self.init() self.lock = IntentLock(identifier: identifier, name: name) } - convenience init(identifier: UUID, cache: LockCache) { + convenience init(id: UUID, cache: LockCache) { self.init() self.lock = IntentLock(identifier: identifier, name: cache.name) @@ -38,7 +38,7 @@ public extension UnlockIntent { @available(iOS 12, iOSApplicationExtension 12.0, watchOS 5.0, *) public extension IntentLock { - convenience init(identifier: UUID, name: String) { + convenience init(id: UUID, name: String) { self.init(identifier: identifier.uuidString, display: name, pronunciationHint: name) } } diff --git a/iOS/LockKit/Model/Store.swift b/iOS/LockKit/Model/Store.swift index 7838ea3f..c8e5ac17 100644 --- a/iOS/LockKit/Model/Store.swift +++ b/iOS/LockKit/Model/Store.swift @@ -156,7 +156,7 @@ public final class Store { // MARK: - Subscript /// Cached information for the specified lock. - public subscript (lock identifier: UUID) -> LockCache? { + public subscript (lock id: UUID) -> LockCache? { get { return locks.value[identifier] } set { @@ -166,7 +166,7 @@ public final class Store { } /// Private Key for the specified lock. - public subscript (key identifier: UUID) -> KeyData? { + public subscript (key id: UUID) -> KeyData? { get { @@ -207,7 +207,7 @@ public final class Store { } /// The Bluetooth LE peripheral for the speciifed lock. - public subscript (peripheral identifier: UUID) -> NativeCentral.Peripheral? { + public subscript (peripheral id: UUID) -> NativeCentral.Peripheral? { return lockInformation.value.first(where: { $0.value.identifier == identifier })?.key } @@ -408,7 +408,7 @@ extension Store: Combine.ObservableObject { } public extension Store { - func device(for identifier: UUID, + func device(for id: UUID, scanDuration: TimeInterval) throws -> LockPeripheral? { assert(Thread.isMainThread == false) @@ -437,7 +437,7 @@ public extension Store { } } - func device(for identifier: UUID) -> LockPeripheral? { + func device(for id: UUID) -> LockPeripheral? { guard let peripheral = self[peripheral: identifier], let lock = self.peripherals.value[peripheral] diff --git a/iOS/Message/MessagesViewController.swift b/iOS/Message/MessagesViewController.swift index c4a513f9..d317e839 100644 --- a/iOS/Message/MessagesViewController.swift +++ b/iOS/Message/MessagesViewController.swift @@ -297,7 +297,7 @@ extension MessagesViewController { struct Item { - let identifier: UUID + let id: UUID let cache: LockCache } } diff --git a/iOS/QuickLook/PreviewViewController.swift b/iOS/QuickLook/PreviewViewController.swift index 464f2761..7f4e6880 100644 --- a/iOS/QuickLook/PreviewViewController.swift +++ b/iOS/QuickLook/PreviewViewController.swift @@ -104,7 +104,7 @@ private extension PreviewViewController { loadChildViewController(viewController) } - func loadLock(_ identifier: UUID) { + func loadLock(_ id: UUID) { // load view controller let viewController = LockViewController.fromStoryboard(with: identifier) diff --git a/iOS/SmartLock/Controller/KeysViewController.swift b/iOS/SmartLock/Controller/KeysViewController.swift index bdeb50f9..d8d9682b 100644 --- a/iOS/SmartLock/Controller/KeysViewController.swift +++ b/iOS/SmartLock/Controller/KeysViewController.swift @@ -228,7 +228,7 @@ final class KeysViewController: UITableViewController { } @discardableResult - internal func select(lock identifier: UUID, animated: Bool = true) -> LockViewController? { + internal func select(lock id: UUID, animated: Bool = true) -> LockViewController? { guard Store.shared[lock: identifier] != nil else { showErrorAlert(LockError.noKey(lock: identifier).localizedDescription) diff --git a/iOS/Watch Extension/Controller/InterfaceController.swift b/iOS/Watch Extension/Controller/InterfaceController.swift index b624611b..01cc51b0 100644 --- a/iOS/Watch Extension/Controller/InterfaceController.swift +++ b/iOS/Watch Extension/Controller/InterfaceController.swift @@ -193,7 +193,7 @@ private extension InterfaceController { struct Item: Equatable { - let identifier: UUID + let id: UUID let cache: LockCache let peripheral: LockPeripheral } diff --git a/iOS/Watch Extension/Controller/Unlock.swift b/iOS/Watch Extension/Controller/Unlock.swift index cb87d3db..83477f67 100644 --- a/iOS/Watch Extension/Controller/Unlock.swift +++ b/iOS/Watch Extension/Controller/Unlock.swift @@ -13,7 +13,7 @@ import CoreLock public extension ActivityInterface where Self: WKInterfaceController { - func unlock(lock identifier: UUID, peripheral: LockPeripheral) { + func unlock(lock id: UUID, peripheral: LockPeripheral) { let needsSync: Bool if let lockCache = Store.shared[lock: identifier] { diff --git a/iOS/Watch Extension/SessionController.swift b/iOS/Watch Extension/SessionController.swift index 5a12c75e..4e4b42ad 100644 --- a/iOS/Watch Extension/SessionController.swift +++ b/iOS/Watch Extension/SessionController.swift @@ -68,7 +68,7 @@ public final class SessionController: NSObject { } } - public func requestKeyData(for identifier: UUID) throws -> KeyData { + public func requestKeyData(for id: UUID) throws -> KeyData { let response = try request(.key(identifier)) switch response { From b9820913a6ce1ec4f466260433961554200bbaa7 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 16 Sep 2022 22:29:15 -0700 Subject: [PATCH 002/229] Updated CoreLock to Swift 5.7 --- .../ConfirmNewKeyCharacteristic.swift | 14 +++--- .../CreateNewKeyCharacteristic.swift | 16 +++---- .../Bluetooth/EventsCharacteristic.swift | 15 ++++--- .../Bluetooth/KeysCharacteristic.swift | 15 ++++--- .../Bluetooth/ListEventsCharacteristic.swift | 34 +++++++++----- .../Bluetooth/ListKeysCharacteristic.swift | 8 +--- .../{ => Bluetooth}/Notification.swift | 4 +- .../Bluetooth/RemoveKeyCharacteristic.swift | 39 +++++++++++----- .../Bluetooth/SetupCharacteristic.swift | 31 +++++-------- Sources/CoreLock/{ => Bluetooth}/TLV.swift | 19 ++++++++ .../Bluetooth/UnlockCharacteristic.swift | 44 ++++++++++++------- Sources/CoreLock/Crypto/Authentication.swift | 14 +++--- Sources/CoreLock/Crypto/Crypto.swift | 14 +++--- Sources/CoreLock/Crypto/EncryptedData.swift | 10 ++--- Sources/CoreLock/EventStore.swift | 4 +- Sources/CoreLock/Extensions/Integer.swift | 6 --- Sources/CoreLock/{ => Extensions}/UUID.swift | 7 ++- Sources/CoreLock/Key.swift | 11 ++--- Sources/CoreLock/LockConfigurationStore.swift | 2 +- Sources/CoreLock/LockState.swift | 2 - .../Networking/CreateNewKeyRequest.swift | 2 +- .../CoreLock/Networking/EventsRequest.swift | 2 +- .../CoreLock/Networking/EventsResponse.swift | 2 +- Sources/CoreLock/Networking/KeysRequest.swift | 2 +- .../CoreLock/Networking/KeysResponse.swift | 2 +- Sources/CoreLock/Networking/URLSession.swift | 30 +------------ Sources/CoreLock/UnlockAction.swift | 1 - .../LockServiceController.swift | 6 +-- Tests/CoreLockTests/CryptoTests.swift | 40 ++++++++--------- Tests/CoreLockTests/GATTProfileTests.swift | 39 +++++++--------- 30 files changed, 228 insertions(+), 207 deletions(-) rename Sources/CoreLock/{ => Bluetooth}/Notification.swift (81%) rename Sources/CoreLock/{ => Bluetooth}/TLV.swift (72%) rename Sources/CoreLock/{ => Extensions}/UUID.swift (73%) diff --git a/Sources/CoreLock/Bluetooth/ConfirmNewKeyCharacteristic.swift b/Sources/CoreLock/Bluetooth/ConfirmNewKeyCharacteristic.swift index 8282adef..6745bc76 100644 --- a/Sources/CoreLock/Bluetooth/ConfirmNewKeyCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/ConfirmNewKeyCharacteristic.swift @@ -10,7 +10,7 @@ import Bluetooth import GATT /// Used to complete new key creation. -public struct ConfirmNewKeyCharacteristic: TLVCharacteristic, Codable, Equatable { +public struct ConfirmNewKeyCharacteristic: TLVEncryptedCharacteristic, Codable, Equatable { public static let uuid = BluetoothUUID(rawValue: "35FD373F-241C-4725-A8A6-C644AADB9A1A")! @@ -18,22 +18,22 @@ public struct ConfirmNewKeyCharacteristic: TLVCharacteristic, Codable, Equatable public static let properties: Bluetooth.BitMaskOptionSet = [.write] - /// Identifier of new key. - public let id: UUID - /// Encrypted payload. public let encryptedData: EncryptedData + public init(encryptedData: EncryptedData) { + self.encryptedData = encryptedData + } + public init(request: ConfirmNewKeyRequest, for key: UUID, sharedSecret: KeyData) throws { let requestData = try type(of: self).encoder.encode(request) - self.encryptedData = try EncryptedData(encrypt: requestData, with: sharedSecret) - self.id = key + self.encryptedData = try EncryptedData(encrypt: requestData, using: sharedSecret, id: .zero) } public func decrypt(with sharedSecret: KeyData) throws -> ConfirmNewKeyRequest { - let data = try encryptedData.decrypt(with: sharedSecret) + let data = try encryptedData.decrypt(using: sharedSecret) guard let value = try? type(of: self).decoder.decode(ConfirmNewKeyRequest.self, from: data) else { throw GATTError.invalidData(data) } return value diff --git a/Sources/CoreLock/Bluetooth/CreateNewKeyCharacteristic.swift b/Sources/CoreLock/Bluetooth/CreateNewKeyCharacteristic.swift index b72fba6d..514039fd 100644 --- a/Sources/CoreLock/Bluetooth/CreateNewKeyCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/CreateNewKeyCharacteristic.swift @@ -10,7 +10,7 @@ import Bluetooth import GATT /// Used to create a new key. -public struct CreateNewKeyCharacteristic: TLVCharacteristic, Codable, Equatable { +public struct CreateNewKeyCharacteristic: TLVEncryptedCharacteristic, Codable, Equatable { public static let uuid = BluetoothUUID(rawValue: "1C9AC449-39DC-4E4D-AE7C-FAF51D35CD7D")! @@ -18,22 +18,22 @@ public struct CreateNewKeyCharacteristic: TLVCharacteristic, Codable, Equatable public static let properties: Bluetooth.BitMaskOptionSet = [.write] - /// Identifier of key making request. - public let id: UUID - /// Encrypted payload. public let encryptedData: EncryptedData - public init(request: CreateNewKeyRequest, for key: UUID, sharedSecret: KeyData) throws { + public init(encryptedData: EncryptedData) { + self.encryptedData = encryptedData + } + + public init(request: CreateNewKeyRequest, using key: KeyData, id: UUID) throws { let requestData = try type(of: self).encoder.encode(request) - self.encryptedData = try EncryptedData(encrypt: requestData, with: sharedSecret) - self.id = key + self.encryptedData = try EncryptedData(encrypt: requestData, using: key, id: id) } public func decrypt(with sharedSecret: KeyData) throws -> CreateNewKeyRequest { - let data = try encryptedData.decrypt(with: sharedSecret) + let data = try encryptedData.decrypt(using: sharedSecret) guard let value = try? type(of: self).decoder.decode(CreateNewKeyRequest.self, from: data) else { throw GATTError.invalidData(data) } return value diff --git a/Sources/CoreLock/Bluetooth/EventsCharacteristic.swift b/Sources/CoreLock/Bluetooth/EventsCharacteristic.swift index a97a4c77..1508f135 100644 --- a/Sources/CoreLock/Bluetooth/EventsCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/EventsCharacteristic.swift @@ -40,10 +40,10 @@ public extension EventsCharacteristic { return value } - static func from(chunks: [Chunk], secret: KeyData) throws -> EventListNotification { + static func from(chunks: [Chunk], using key: KeyData) throws -> EventListNotification { let encryptedData = try from(chunks: chunks) - let data = try encryptedData.decrypt(with: secret) + let data = try encryptedData.decrypt(using: key) guard let value = try? decoder.decode(EventListNotification.self, from: data) else { throw GATTError.invalidData(data) } return value @@ -56,12 +56,15 @@ public extension EventsCharacteristic { return chunks.map { .init(chunk: $0) } } - static func from(_ value: EventListNotification, - sharedSecret: KeyData, - maximumUpdateValueLength: Int) throws -> [EventsCharacteristic] { + static func from( + _ value: EventListNotification, + id: UUID, + key: KeyData, + maximumUpdateValueLength: Int + ) throws -> [EventsCharacteristic] { let data = try encoder.encode(value) - let encryptedData = try EncryptedData(encrypt: data, with: sharedSecret) + let encryptedData = try EncryptedData(encrypt: data, using: key, id: id) return try from(encryptedData, maximumUpdateValueLength: maximumUpdateValueLength) } } diff --git a/Sources/CoreLock/Bluetooth/KeysCharacteristic.swift b/Sources/CoreLock/Bluetooth/KeysCharacteristic.swift index 997e35d2..866c3a8d 100644 --- a/Sources/CoreLock/Bluetooth/KeysCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/KeysCharacteristic.swift @@ -40,10 +40,10 @@ public extension KeysCharacteristic { return value } - static func from(chunks: [Chunk], secret: KeyData) throws -> KeyListNotification { + static func from(chunks: [Chunk], using key: KeyData) throws -> KeyListNotification { let encryptedData = try from(chunks: chunks) - let data = try encryptedData.decrypt(with: secret) + let data = try encryptedData.decrypt(using: key) guard let value = try? decoder.decode(KeyListNotification.self, from: data) else { throw GATTError.invalidData(data) } return value @@ -56,12 +56,15 @@ public extension KeysCharacteristic { return chunks.map { KeysCharacteristic(chunk: $0) } } - static func from(_ value: KeyListNotification, - sharedSecret: KeyData, - maximumUpdateValueLength: Int) throws -> [KeysCharacteristic] { + static func from( + _ value: KeyListNotification, + id: UUID, + key: KeyData, + maximumUpdateValueLength: Int + ) throws -> [KeysCharacteristic] { let data = try encoder.encode(value) - let encryptedData = try EncryptedData(encrypt: data, with: sharedSecret) + let encryptedData = try EncryptedData(encrypt: data, using: key, id: id) return try from(encryptedData, maximumUpdateValueLength: maximumUpdateValueLength) } } diff --git a/Sources/CoreLock/Bluetooth/ListEventsCharacteristic.swift b/Sources/CoreLock/Bluetooth/ListEventsCharacteristic.swift index 9ca6960c..90d717c9 100644 --- a/Sources/CoreLock/Bluetooth/ListEventsCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/ListEventsCharacteristic.swift @@ -10,7 +10,7 @@ import Bluetooth import GATT /// List events request -public struct ListEventsCharacteristic: TLVCharacteristic, Codable, Equatable { +public struct ListEventsCharacteristic: TLVEncryptedCharacteristic, Codable, Equatable { public static let uuid = BluetoothUUID(rawValue: "98433693-D5BB-44A4-A929-63B453C3A8C4")! @@ -18,21 +18,33 @@ public struct ListEventsCharacteristic: TLVCharacteristic, Codable, Equatable { public static let properties: Bluetooth.BitMaskOptionSet = [.write] - /// Identifier of key making request. - public let id: UUID + public let encryptedData: EncryptedData - /// HMAC of key and nonce, and HMAC message - public let authentication: Authentication + public init(encryptedData: EncryptedData) { + self.encryptedData = encryptedData + } + + public init(request: ListEventsRequest, using key: KeyData, id: UUID) throws { + + let requestData = try type(of: self).encoder.encode(request) + self.encryptedData = try EncryptedData(encrypt: requestData, using: key, id: id) + } + + public func decrypt(with key: KeyData) throws -> ListEventsRequest { + + let data = try encryptedData.decrypt(using: key) + guard let value = try? type(of: self).decoder.decode(ListEventsRequest.self, from: data) + else { throw GATTError.invalidData(data) } + return value + } +} + +public struct ListEventsRequest: Codable, Equatable { /// Fetch limit for events to view. public let fetchRequest: LockEvent.FetchRequest? - public init(id: UUID, - authentication: Authentication, - fetchRequest: LockEvent.FetchRequest? = nil) { - - self.id = id - self.authentication = authentication + public init(fetchRequest: LockEvent.FetchRequest? = nil) { self.fetchRequest = fetchRequest } } diff --git a/Sources/CoreLock/Bluetooth/ListKeysCharacteristic.swift b/Sources/CoreLock/Bluetooth/ListKeysCharacteristic.swift index f5007e97..08f1323a 100644 --- a/Sources/CoreLock/Bluetooth/ListKeysCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/ListKeysCharacteristic.swift @@ -18,16 +18,10 @@ public struct ListKeysCharacteristic: TLVCharacteristic, Codable, Equatable { public static let properties: Bluetooth.BitMaskOptionSet = [.write] - /// Identifier of key making request. - public let id: UUID - /// HMAC of key and nonce, and HMAC message public let authentication: Authentication - public init(id: UUID, - authentication: Authentication) { - - self.id = id + public init(authentication: Authentication) { self.authentication = authentication } } diff --git a/Sources/CoreLock/Notification.swift b/Sources/CoreLock/Bluetooth/Notification.swift similarity index 81% rename from Sources/CoreLock/Notification.swift rename to Sources/CoreLock/Bluetooth/Notification.swift index 951d2d6c..c77ed114 100644 --- a/Sources/CoreLock/Notification.swift +++ b/Sources/CoreLock/Bluetooth/Notification.swift @@ -19,11 +19,11 @@ public protocol GATTEncryptedNotification: GATTProfileCharacteristic { static func from(chunks: [Chunk]) throws -> EncryptedData - static func from(chunks: [Chunk], secret: KeyData) throws -> Notification + static func from(chunks: [Chunk], using key: KeyData) throws -> Notification static func from(_ value: EncryptedData, maximumUpdateValueLength: Int) throws -> [Self] - static func from(_ value: Notification, sharedSecret: KeyData, maximumUpdateValueLength: Int) throws -> [Self] + static func from(_ value: Notification, id: UUID, key: KeyData, maximumUpdateValueLength: Int) throws -> [Self] } public protocol GATTEncryptedNotificationValue { diff --git a/Sources/CoreLock/Bluetooth/RemoveKeyCharacteristic.swift b/Sources/CoreLock/Bluetooth/RemoveKeyCharacteristic.swift index 02b83d07..73596e40 100644 --- a/Sources/CoreLock/Bluetooth/RemoveKeyCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/RemoveKeyCharacteristic.swift @@ -10,7 +10,7 @@ import Bluetooth import GATT /// Remove the specified key. -public struct RemoveKeyCharacteristic: TLVCharacteristic, Codable, Equatable { +public struct RemoveKeyCharacteristic: TLVEncryptedCharacteristic, Codable, Equatable { public static let uuid = BluetoothUUID(rawValue: "2DB6C1AF-8FFD-4F7F-9B5A-F0BC9662F9BB")! @@ -18,26 +18,41 @@ public struct RemoveKeyCharacteristic: TLVCharacteristic, Codable, Equatable { public static let properties: Bluetooth.BitMaskOptionSet = [.write] - /// Identifier of key making request. - public let id: UUID + public let encryptedData: EncryptedData + + public init(encryptedData: EncryptedData) { + self.encryptedData = encryptedData + } + + public init(request: RemoveKeyRequest, using key: KeyData, id: UUID) throws { + + let requestData = try type(of: self).encoder.encode(request) + self.encryptedData = try EncryptedData(encrypt: requestData, using: key, id: id) + } + + public func decrypt(using key: KeyData) throws -> RemoveKeyRequest { + + let data = try encryptedData.decrypt(using: key) + guard let value = try? type(of: self).decoder.decode(RemoveKeyRequest.self, from: data) + else { throw GATTError.invalidData(data) } + return value + } +} + +// MARK: - Supporting Types + +public struct RemoveKeyRequest: Equatable, Codable { /// Key to remove. - public let key: UUID + public let id: UUID /// Type of key public let type: KeyType - /// HMAC of key and nonce, and HMAC message - public let authentication: Authentication - public init(id: UUID, - key: UUID, - type: KeyType, - authentication: Authentication) { + type: KeyType) { self.id = id - self.key = key self.type = type - self.authentication = authentication } } diff --git a/Sources/CoreLock/Bluetooth/SetupCharacteristic.swift b/Sources/CoreLock/Bluetooth/SetupCharacteristic.swift index 603a340c..eba398b8 100644 --- a/Sources/CoreLock/Bluetooth/SetupCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/SetupCharacteristic.swift @@ -10,44 +10,35 @@ import Bluetooth import GATT /// Used for initial lock setup. -public struct SetupCharacteristic: TLVCharacteristic, Equatable { +public struct SetupCharacteristic: TLVEncryptedCharacteristic, Codable, Equatable { - public static let uuid = BluetoothUUID(rawValue: "129E401C-044D-11E6-8FA9-09AB70D5A8C7")! + public static var uuid: BluetoothUUID { BluetoothUUID(rawValue: "129E401C-044D-11E6-8FA9-09AB70D5A8C7")! } - public static let service: GATTProfileService.Type = LockService.self + public static var service: GATTProfileService.Type { LockService.self } - public static let properties: Bluetooth.BitMaskOptionSet = [.write] + public static var properties: Bluetooth.BitMaskOptionSet { [.write] } public let encryptedData: EncryptedData + public init(encryptedData: EncryptedData) { + self.encryptedData = encryptedData + } + public init(request: SetupRequest, sharedSecret: KeyData) throws { let requestData = try type(of: self).encoder.encode(request) - self.encryptedData = try EncryptedData(encrypt: requestData, with: sharedSecret) + self.encryptedData = try EncryptedData(encrypt: requestData, using: sharedSecret, id: .zero) } - public func decrypt(with sharedSecret: KeyData) throws -> SetupRequest { + public func decrypt(using sharedSecret: KeyData) throws -> SetupRequest { - let data = try encryptedData.decrypt(with: sharedSecret) + let data = try encryptedData.decrypt(using: sharedSecret) guard let value = try? type(of: self).decoder.decode(SetupRequest.self, from: data) else { throw GATTError.invalidData(data) } return value } } -// MARK: - Codable - -extension SetupCharacteristic: Codable { - - public init(from decoder: Decoder) throws { - self.encryptedData = try EncryptedData(from: decoder) - } - - public func encode(to encoder: Encoder) throws { - try self.encryptedData.encode(to: encoder) - } -} - // MARK: - Supporting Types public struct SetupRequest: Equatable, Codable { diff --git a/Sources/CoreLock/TLV.swift b/Sources/CoreLock/Bluetooth/TLV.swift similarity index 72% rename from Sources/CoreLock/TLV.swift rename to Sources/CoreLock/Bluetooth/TLV.swift index 5b2be5d0..6135ad23 100644 --- a/Sources/CoreLock/TLV.swift +++ b/Sources/CoreLock/Bluetooth/TLV.swift @@ -58,3 +58,22 @@ public extension TLVCharacteristic where Self: Codable { return try! Self.encoder.encode(self) } } + +public protocol TLVEncryptedCharacteristic: TLVCharacteristic { + + var encryptedData: EncryptedData { get } + + init(encryptedData: EncryptedData) +} + +public extension TLVEncryptedCharacteristic where Self: Codable { + + init(from decoder: Decoder) throws { + let encryptedData = try EncryptedData(from: decoder) + self.init(encryptedData: encryptedData) + } + + func encode(to encoder: Encoder) throws { + try self.encryptedData.encode(to: encoder) + } +} diff --git a/Sources/CoreLock/Bluetooth/UnlockCharacteristic.swift b/Sources/CoreLock/Bluetooth/UnlockCharacteristic.swift index e85adbbc..94a07dc0 100644 --- a/Sources/CoreLock/Bluetooth/UnlockCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/UnlockCharacteristic.swift @@ -10,29 +10,43 @@ import Bluetooth import GATT /// Used to unlock door. -public struct UnlockCharacteristic: TLVCharacteristic, Codable, Equatable { +public struct UnlockCharacteristic: TLVEncryptedCharacteristic, Codable, Equatable { - public static let uuid = BluetoothUUID(rawValue: "265B3EC0-044D-11E6-90F2-09AB70D5A8C7")! + public static var uuid: BluetoothUUID { BluetoothUUID(rawValue: "265B3EC0-044D-11E6-90F2-09AB70D5A8C7")! } - public static let service: GATTProfileService.Type = LockService.self + public static var service: GATTProfileService.Type { LockService.self } - public static let properties: Bluetooth.BitMaskOptionSet = [.write] + public static var properties: Bluetooth.BitMaskOptionSet { [.write] } - /// Identifier of key making request. - public let id: UUID + public let encryptedData: EncryptedData - /// Unlock action. - public let action: UnlockAction + public init(encryptedData: EncryptedData) { + self.encryptedData = encryptedData + } - /// HMAC of key and nonce, and HMAC message - public let authentication: Authentication + public init(request: UnlockRequest, using key: KeyData, id: UUID) throws { + + let requestData = try type(of: self).encoder.encode(request) + self.encryptedData = try EncryptedData(encrypt: requestData, using: key, id: id) + } - public init(id: UUID, - action: UnlockAction = .default, - authentication: Authentication) { + public func decrypt(with sharedSecret: KeyData) throws -> UnlockRequest { - self.id = id + let data = try encryptedData.decrypt(using: sharedSecret) + guard let value = try? type(of: self).decoder.decode(UnlockRequest.self, from: data) + else { throw GATTError.invalidData(data) } + return value + } +} + +// MARK: - Supporting Types + +public struct UnlockRequest: Equatable, Codable { + + /// Unlock action. + public let action: UnlockAction + + public init(action: UnlockAction = .default) { self.action = action - self.authentication = authentication } } diff --git a/Sources/CoreLock/Crypto/Authentication.swift b/Sources/CoreLock/Crypto/Authentication.swift index dafed141..88261ca0 100644 --- a/Sources/CoreLock/Crypto/Authentication.swift +++ b/Sources/CoreLock/Crypto/Authentication.swift @@ -17,7 +17,7 @@ public struct Authentication: Equatable, Codable { message: AuthenticationMessage) { self.message = message - self.signedData = AuthenticationData(key: key, message: message) + self.signedData = AuthenticationData(message, using: key) } public func isAuthenticated(using key: KeyData) -> Bool { @@ -31,27 +31,31 @@ public struct AuthenticationMessage: Equatable, Codable { public let date: Date public let nonce: Nonce - + public let digest: Digest + public let id: UUID + public init( date: Date = Date(), nonce: Nonce = Nonce(), - digest: Digest + digest: Digest, + id: UUID ) { self.date = date self.nonce = nonce self.digest = digest + self.id = id } } public extension AuthenticationData { - init(key: KeyData, message: AuthenticationMessage) { + init(_ message: AuthenticationMessage, using key: KeyData) { self = authenticationCode(for: message, using: key) } func isAuthenticated(_ message: AuthenticationMessage, using key: KeyData) -> Bool { - return data == AuthenticationData(key: key, message: message).data + return data == AuthenticationData(message, using: key).data } } diff --git a/Sources/CoreLock/Crypto/Crypto.swift b/Sources/CoreLock/Crypto/Crypto.swift index 26f6e919..87e6a825 100644 --- a/Sources/CoreLock/Crypto/Crypto.swift +++ b/Sources/CoreLock/Crypto/Crypto.swift @@ -29,10 +29,11 @@ internal func authenticationCode(for message: AuthenticationMessage, using key: } /// Encrypt data -internal func encrypt(_ data: Data, using key: KeyData) throws -> Data { +internal func encrypt(_ data: Data, using key: KeyData, nonce: Nonce, authentication: AuthenticationMessage) throws -> Data { + let encoder = TLVEncoder.lock + let authenticatedData = try! encoder.encode(authentication) do { - let authenticationData = Data(SHA512.hash(data: data)) - let sealed = try ChaChaPoly.seal(data, using: SymmetricKey(key), nonce: .init(), authenticating: authenticationData) + let sealed = try ChaChaPoly.seal(data, using: SymmetricKey(key), nonce: ChaChaPoly.Nonce(nonce), authenticating: authenticatedData) return sealed.combined } catch { throw AuthenticationError.encryptionError(error) @@ -41,10 +42,11 @@ internal func encrypt(_ data: Data, using key: KeyData) throws -> Data { /// Decrypt data internal func decrypt(_ data: Data, using key: KeyData, authentication: AuthenticationMessage) throws -> Data { + let encoder = TLVEncoder.lock + let authenticatedData = try! encoder.encode(authentication) do { - let authenticationData = Data(SHA512.hash(data: data)) let sealed = try ChaChaPoly.SealedBox(combined: data) - let decrypted = try ChaChaPoly.open(sealed, using: SymmetricKey(key), authenticating: authenticationData) + let decrypted = try ChaChaPoly.open(sealed, using: SymmetricKey(key), authenticating: authenticatedData) return decrypted } catch { throw AuthenticationError.decryptionError(error) @@ -90,7 +92,7 @@ public extension Digest { public extension AuthenticationData { - static var length: Int { 32 } + static var length: Int { 64 } } internal extension SymmetricKey { diff --git a/Sources/CoreLock/Crypto/EncryptedData.swift b/Sources/CoreLock/Crypto/EncryptedData.swift index 9b05a70a..f308ac69 100644 --- a/Sources/CoreLock/Crypto/EncryptedData.swift +++ b/Sources/CoreLock/Crypto/EncryptedData.swift @@ -18,20 +18,20 @@ public struct EncryptedData: Equatable, Codable { public extension EncryptedData { - init(encrypt data: Data, with key: KeyData) throws { + init(encrypt data: Data, using key: KeyData, id: UUID) throws { let digest = Digest(hash: data) - let message = AuthenticationMessage(digest: digest) - let encryptedData = try encrypt(data, using: key, nonce: message.nonce) + let message = AuthenticationMessage(digest: digest, id: id) + let encryptedData = try encrypt(data, using: key, nonce: message.nonce, authentication: message) let authentication = Authentication(key: key, message: message) self.authentication = authentication self.encryptedData = encryptedData } - func decrypt(with key: KeyData) throws -> Data { + func decrypt(using key: KeyData) throws -> Data { // validate HMAC guard authentication.isAuthenticated(using: key) else { throw AuthenticationError.invalidAuthentication } // attempt to decrypt - return try CoreLock.decrypt(encryptedData, using: key) + return try CoreLock.decrypt(encryptedData, using: key, authentication: authentication.message) } } diff --git a/Sources/CoreLock/EventStore.swift b/Sources/CoreLock/EventStore.swift index 7ddf9e40..7073ede8 100644 --- a/Sources/CoreLock/EventStore.swift +++ b/Sources/CoreLock/EventStore.swift @@ -8,7 +8,7 @@ import Foundation -public protocol LockEventStore: class { +public protocol LockEventStore: AnyObject { func fetch(_ fetchRequest: LockEvent.FetchRequest) throws -> [LockEvent] @@ -64,7 +64,7 @@ public extension LockEvent { public var start: Date? public var end: Date? - + public init(keys: [UUID]?, start: Date? = nil, end: Date? = nil) { diff --git a/Sources/CoreLock/Extensions/Integer.swift b/Sources/CoreLock/Extensions/Integer.swift index b8e527c8..cd5f2d03 100644 --- a/Sources/CoreLock/Extensions/Integer.swift +++ b/Sources/CoreLock/Extensions/Integer.swift @@ -11,13 +11,11 @@ internal extension UInt16 { /// Initializes value from two bytes. init(bytes: (UInt8, UInt8)) { - self = unsafeBitCast(bytes, to: UInt16.self) } /// Converts to two bytes. var bytes: (UInt8, UInt8) { - return unsafeBitCast(self, to: (UInt8, UInt8).self) } } @@ -26,13 +24,11 @@ internal extension UInt32 { /// Initializes value from four bytes. init(bytes: (UInt8, UInt8, UInt8, UInt8)) { - self = unsafeBitCast(bytes, to: UInt32.self) } /// Converts to four bytes. var bytes: (UInt8, UInt8, UInt8, UInt8) { - return unsafeBitCast(self, to: (UInt8, UInt8, UInt8, UInt8).self) } } @@ -41,13 +37,11 @@ internal extension UInt64 { /// Initializes value from four bytes. init(bytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)) { - self = unsafeBitCast(bytes, to: UInt64.self) } /// Converts to eight bytes. var bytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) { - return unsafeBitCast(self, to: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8).self) } } diff --git a/Sources/CoreLock/UUID.swift b/Sources/CoreLock/Extensions/UUID.swift similarity index 73% rename from Sources/CoreLock/UUID.swift rename to Sources/CoreLock/Extensions/UUID.swift index ed7e6b00..7159c0de 100644 --- a/Sources/CoreLock/UUID.swift +++ b/Sources/CoreLock/Extensions/UUID.swift @@ -9,9 +9,14 @@ import Foundation public extension UUID { - + /// iBeacon Lock Notification static var lockNotificationBeacon: UUID { return UUID(uuidString: "F6AC86F3-A97D-4FA7-8668-C8ECFD1E538D")! } } + +internal extension UUID { + + static var zero: UUID { UUID(uuidString: "00000000-0000-0000-0000-000000000000")! } +} diff --git a/Sources/CoreLock/Key.swift b/Sources/CoreLock/Key.swift index 909023cb..4f48e6d0 100644 --- a/Sources/CoreLock/Key.swift +++ b/Sources/CoreLock/Key.swift @@ -23,11 +23,12 @@ public struct Key: Identifiable, Codable, Equatable, Hashable { /// Key's permissions. public let permission: Permission - public init(id: UUID = UUID(), - name: String = "", - created: Date = Date(), - permission: Permission) { - + public init( + id: UUID = UUID(), + name: String = "", + created: Date = Date(), + permission: Permission + ) { self.id = id self.name = name self.created = created diff --git a/Sources/CoreLock/LockConfigurationStore.swift b/Sources/CoreLock/LockConfigurationStore.swift index 87ba52e0..66722acd 100644 --- a/Sources/CoreLock/LockConfigurationStore.swift +++ b/Sources/CoreLock/LockConfigurationStore.swift @@ -8,7 +8,7 @@ import Foundation /// Lock Configuration Storage -public protocol LockConfigurationStore: class { +public protocol LockConfigurationStore: AnyObject { var configuration: LockConfiguration { get } diff --git a/Sources/CoreLock/LockState.swift b/Sources/CoreLock/LockState.swift index b3b5ded6..27a06bd5 100644 --- a/Sources/CoreLock/LockState.swift +++ b/Sources/CoreLock/LockState.swift @@ -5,8 +5,6 @@ // Created by Alsey Coleman Miller on 8/11/18. // -import Bluetooth - public enum UnlockState: UInt8, BitMaskOption { /// Unlocked. diff --git a/Sources/CoreLock/Networking/CreateNewKeyRequest.swift b/Sources/CoreLock/Networking/CreateNewKeyRequest.swift index e7f69e04..607c998a 100644 --- a/Sources/CoreLock/Networking/CreateNewKeyRequest.swift +++ b/Sources/CoreLock/Networking/CreateNewKeyRequest.swift @@ -59,7 +59,7 @@ public extension CreateNewKeyNetServiceRequest { with key: KeyData, decoder: JSONDecoder = JSONDecoder()) throws -> CreateNewKeyRequest { - let jsonData = try encryptedData.decrypt(with: key) + let jsonData = try encryptedData.decrypt(using: key) return try decoder.decode(CreateNewKeyRequest.self, from: jsonData) } } diff --git a/Sources/CoreLock/Networking/EventsRequest.swift b/Sources/CoreLock/Networking/EventsRequest.swift index 0de60375..6893ba05 100644 --- a/Sources/CoreLock/Networking/EventsRequest.swift +++ b/Sources/CoreLock/Networking/EventsRequest.swift @@ -143,7 +143,7 @@ public extension LockNetService.Client { let response = try? jsonDecoder.decode(EventsResponse.self, from: jsonData) else { throw LockNetService.Error.invalidResponse } - let keys = try response.decrypt(with: key.secret, decoder: jsonDecoder) + let keys = try response.decrypt(using: key.secret, decoder: jsonDecoder) return keys } } diff --git a/Sources/CoreLock/Networking/EventsResponse.swift b/Sources/CoreLock/Networking/EventsResponse.swift index a53b6814..baddf206 100644 --- a/Sources/CoreLock/Networking/EventsResponse.swift +++ b/Sources/CoreLock/Networking/EventsResponse.swift @@ -41,7 +41,7 @@ public extension EventsResponse { func decrypt(with key: KeyData, decoder: JSONDecoder = JSONDecoder()) throws -> EventsList { - let data = try encryptedData.decrypt(with: key) + let data = try encryptedData.decrypt(using: key) return try decoder.decode(EventsList.self, from: data) } } diff --git a/Sources/CoreLock/Networking/KeysRequest.swift b/Sources/CoreLock/Networking/KeysRequest.swift index aff95184..c2ee6aca 100644 --- a/Sources/CoreLock/Networking/KeysRequest.swift +++ b/Sources/CoreLock/Networking/KeysRequest.swift @@ -63,7 +63,7 @@ public extension LockNetService.Client { let response = try? jsonDecoder.decode(KeysResponse.self, from: jsonData) else { throw LockNetService.Error.invalidResponse } - let keys = try response.decrypt(with: key.secret, decoder: jsonDecoder) + let keys = try response.decrypt(using: key.secret, decoder: jsonDecoder) return keys } } diff --git a/Sources/CoreLock/Networking/KeysResponse.swift b/Sources/CoreLock/Networking/KeysResponse.swift index 27814a6a..a7faabf0 100644 --- a/Sources/CoreLock/Networking/KeysResponse.swift +++ b/Sources/CoreLock/Networking/KeysResponse.swift @@ -41,7 +41,7 @@ public extension KeysResponse { func decrypt(with key: KeyData, decoder: JSONDecoder = JSONDecoder()) throws -> KeysList { - let data = try encryptedData.decrypt(with: key) + let data = try encryptedData.decrypt(using: key) return try decoder.decode(KeysList.self, from: data) } } diff --git a/Sources/CoreLock/Networking/URLSession.swift b/Sources/CoreLock/Networking/URLSession.swift index 33ac6ce8..b7e16b19 100644 --- a/Sources/CoreLock/Networking/URLSession.swift +++ b/Sources/CoreLock/Networking/URLSession.swift @@ -15,33 +15,5 @@ import FoundationNetworking 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/UnlockAction.swift b/Sources/CoreLock/UnlockAction.swift index 1e025b0a..e88d5144 100644 --- a/Sources/CoreLock/UnlockAction.swift +++ b/Sources/CoreLock/UnlockAction.swift @@ -7,7 +7,6 @@ import Foundation import TLVCoding -import Bluetooth /// Unlock Action public enum UnlockAction: UInt8, BitMaskOption { diff --git a/Sources/CoreLockGATTServer/LockServiceController.swift b/Sources/CoreLockGATTServer/LockServiceController.swift index 2455bf3e..d8013e1a 100644 --- a/Sources/CoreLockGATTServer/LockServiceController.swift +++ b/Sources/CoreLockGATTServer/LockServiceController.swift @@ -297,7 +297,7 @@ public final class LockGATTServiceController : else { print("Authentication expired \(timestamp) < \(now)"); return } // decrypt request - let setupRequest = try setup.decrypt(with: sharedSecret) + let setupRequest = try setup.decrypt(using: sharedSecret) // create owner key let ownerKey = Key(setup: setupRequest) @@ -386,7 +386,7 @@ public final class LockGATTServiceController : } // decrypt - let request = try characteristic.decrypt(with: secret) + let request = try characteristic.decrypt(using: secret) let newKey = NewKey(request: request) try self.authorization.add(newKey, secret: request.secret) @@ -423,7 +423,7 @@ public final class LockGATTServiceController : else { print("Authentication expired \(timestamp) < \(now)"); return } // decrypt - let request = try characteristic.decrypt(with: secret) + let request = try characteristic.decrypt(using: secret) let keySecret = request.secret let key = Key( identifier: newKey.identifier, diff --git a/Tests/CoreLockTests/CryptoTests.swift b/Tests/CoreLockTests/CryptoTests.swift index 539dcd6a..2b6b1f00 100644 --- a/Tests/CoreLockTests/CryptoTests.swift +++ b/Tests/CoreLockTests/CryptoTests.swift @@ -12,45 +12,45 @@ import XCTest final class CryptoTests: XCTestCase { - static let allTests = [ - ("testHMAC", testHMAC), - ("testEncrypt", testEncrypt), - ("testFailEncrypt", testFailEncrypt), - ("testNonce", testNonce) - ] - func testHMAC() { + let id = UUID() let key = KeyData() let nonce = Nonce() let timestamp = Date() - let message = AuthenticationMessage(date: timestamp, nonce: nonce) - let hmac = HMAC(key: key, message: message) - XCTAssert(hmac.data == HMAC(key: key, message: message).data, "Values must be consistent") + let message = AuthenticationMessage(date: timestamp, nonce: nonce, digest: Digest(hash: Data()), id: id) + let authentication = Authentication(key: key, message: message) + XCTAssert(authentication.isAuthenticated(using: key), "Values must be consistent") } - func testEncrypt() { + func testEncrypt() throws { + let id = UUID() let key = KeyData() + let randomData = KeyData().data + let timestamp = Date() let nonce = Nonce() - let (encryptedData, iv) = try! encrypt(key: key.data, data: nonce.data) - let decryptedData = try! decrypt(key: key.data, iv: iv, data: encryptedData) - XCTAssert(nonce.data == decryptedData) + let message = AuthenticationMessage(date: timestamp, nonce: nonce, digest: Digest(hash: randomData), id: id) + let encryptedData = try encrypt(randomData, using: key, nonce: nonce, authentication: message) + let decryptedData = try decrypt(encryptedData, using: key, authentication: message) + XCTAssertEqual(randomData, decryptedData) } - func testFailEncrypt() { + func testFailEncrypt() throws { let key = KeyData() let key2 = KeyData() - XCTAssert(key != key2) + XCTAssertNotEqual(key, key2) + let id = UUID() + let randomData = KeyData().data + let timestamp = Date() let nonce = Nonce() - let (encryptedData, iv) = try! encrypt(key: key.data, data: nonce.data) - let decryptedData = try! decrypt(key: key2.data, iv: iv, data: encryptedData) - XCTAssert(nonce.data != decryptedData) + let message = AuthenticationMessage(date: timestamp, nonce: nonce, digest: Digest(hash: randomData), id: id) + let encryptedData = try encrypt(randomData, using: key, nonce: nonce, authentication: message) + XCTAssertThrowsError(try decrypt(encryptedData, using: key2, authentication: message)) } func testNonce() { - (0 ... 100).forEach { _ in XCTAssertNotEqual(Nonce(), Nonce()) } } } diff --git a/Tests/CoreLockTests/GATTProfileTests.swift b/Tests/CoreLockTests/GATTProfileTests.swift index 318e37e3..8ab092f7 100644 --- a/Tests/CoreLockTests/GATTProfileTests.swift +++ b/Tests/CoreLockTests/GATTProfileTests.swift @@ -26,35 +26,30 @@ final class GATTProfileTests: XCTestCase { XCTAssertEqual(information, decoded) } - func testUnlock() { + func testUnlock() throws { let key = (id: UUID(), secret: KeyData()) - - let authentication = Authentication(key: key.secret, message: AuthenticationMessage(digest: Digest(hash: <#T##Data#>))) - - let characteristic = UnlockCharacteristic( - id: key.id, - authentication: authentication - ) - - guard let decoded = UnlockCharacteristic(data: characteristic.data) + let request = UnlockRequest(action: .default) + let characteristic = try UnlockCharacteristic(request: request, using: key.secret, id: key.id) + guard let decodedCharacteristic = UnlockCharacteristic(data: characteristic.data) else { XCTFail("Could not parse bytes"); return } - - XCTAssertEqual(characteristic, decoded) - XCTAssertEqual(try! TLVEncoder.lock.encode(decoded.authentication), - try! TLVEncoder.lock.encode(authentication)) - XCTAssert(decoded.authentication.isAuthenticated(using: key.secret)) - XCTAssert(characteristic.authentication.isAuthenticated(using: key.secret)) - XCTAssertFalse(Authentication(key: KeyData()).isAuthenticated(using: key.secret)) + let decodedRequest = try decodedCharacteristic.decrypt(with: key.secret) + XCTAssertEqual(decodedRequest, request) + XCTAssertEqual(characteristic, decodedCharacteristic) + XCTAssertEqual(characteristic.encryptedData, decodedCharacteristic.encryptedData) + XCTAssertEqual(characteristic.encryptedData.authentication, decodedCharacteristic.encryptedData.authentication) + XCTAssertEqual(characteristic.encryptedData.authentication.message, decodedCharacteristic.encryptedData.authentication.message) + XCTAssertEqual(characteristic.encryptedData.authentication.signedData, decodedCharacteristic.encryptedData.authentication.signedData) + XCTAssert(decodedCharacteristic.encryptedData.authentication.isAuthenticated(using: key.secret)) + XCTAssert(characteristic.encryptedData.authentication.isAuthenticated(using: key.secret)) + //XCTAssertFalse(Authentication(key: key.secret, message: characteristic.encryptedData.authentication.message).isAuthenticated(using: key.secret)) } - func testSetup() { + func testSetup() throws { let deviceSharedSecret = KeyData() - let request = SetupRequest() - - let characteristic = try! SetupCharacteristic(request: request, sharedSecret: deviceSharedSecret) + let characteristic = try SetupCharacteristic(request: request, sharedSecret: deviceSharedSecret) guard let decoded = SetupCharacteristic(data: characteristic.data) else { XCTFail("Could not parse bytes"); return } @@ -62,7 +57,7 @@ final class GATTProfileTests: XCTestCase { XCTAssertEqual(try! TLVEncoder.lock.encode(decoded.encryptedData), try! TLVEncoder.lock.encode(characteristic.encryptedData)) - let decrypted = try! decoded.decrypt(with: deviceSharedSecret) + let decrypted = try decoded.decrypt(using: deviceSharedSecret) XCTAssertEqual(request, decrypted) XCTAssertEqual(request.id, decrypted.id) From e17ec0a46e44aa6bd3733aef8998b76d0560e300 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 16 Sep 2022 22:30:50 -0700 Subject: [PATCH 003/229] Updated `StyleKit` --- Assets/StyleKit.pcvd | Bin 509231 -> 509270 bytes iOS/LockKit/View/StyleKit.swift | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Assets/StyleKit.pcvd b/Assets/StyleKit.pcvd index 6b1ad1c59ff3496cdd6b74f00e4d1695a827bd5d..61192f32ebf41d8eec257f1db463c6b220696e70 100644 GIT binary patch literal 509270 zcmZs@cU;=n+yDPYL{uCs8Oq+sUMi>{Zbc9QMM05`D57y>4^5h#G^^Q7Gn+QMnQd0n z>}I#wtJ!-uv)Nyg^mBji`|hLrLBTp7aS6h26qs`sy^K^A00nn%gjqqfHS?3$z^J@P! zx(ufPu|OOU55Rx~AQ4Cck^wk?0FVF*Km#cN2EYP101u=BX#fEr0wjP8Pyi}G1Lyz) zU;-?F4R8Q1kPh$wJ|F;ufCvyr{?Y*%AO{qYQYMfEC;=6a4dg_g)Dc@TkOydi{D@5< zPy`e=!c+PNJA>|!uPofz9*MQBBfqV`ud@*jZ}qjdbqvtE!mfa?)feun@9y*&gZ+J> zl8&I)-tO|WL>dgPR$pXd-ZoG7C!3ltd5H8Ha&>kU^>1(M=&CkV%?g|CNzRt>UXh2f$x>ZiR#@NJFX;bo+{g}JXIrSdE3%mRz9v`SU|UCIdTs5#4p&z&954^)?DDn# zpXpg$o#w6qA)hYTW%snTw!6Xu{tcq9t0U-&WY*l&*6&lOjc`n7q*%}v?rQ7qsO$(v z5*F#TEo}D&JKIC90iTTOJOAzM-^>OB;kFK+*XrsBN5=oOE}#48?(Ax7{gN1KFx=A5 z7Yf;eZJqyS)YjJ7|KDOSeh3^C*8@Qe0ebNRdtwk{_Q%G?!NEPTJ<+kf_@Z8Hb`J>7 zfdlYJ4Kx-W)dTLu502}_4~G}SZEFyO5}*_)0}Ox#s0L~PD^Ls60Un?O=mdrXTY+uB zPGAqPANU7&4SWEBK+zxwC?13WHGmu-H^>L-0Cj=}fqFs1L8Cz9K;uCZKodb_${pp2@5ZH6&_i)QG6B zqDDrIiy9v_A!0l0+3r+`%z+$ijoB_@RXMvSqHCO}A0~djd!6o1dupVpxSAxx8 z54Z)~2_6I<0Ui&Y2L1)S8N40*D|iQZH+Vnz5AZ4QY492FMetwXOW^C^8{nJZTj2ZP z2jGX`r{HJc=it}iH{iG6chNvJJQ^2GiRMI$qqCy(qVuDRqpi`k(e=^JXm4~=bW3z7 zx-GghdO-BB=;6`RqQ8k=8ofMvPxRjC1JPHauSVaDeiQvR`Xht_p+cAtK12YKLSzs* zLPhSWmpAPz_)#1CnKgdlw&9gzNz0g$1PVUS6X>5v(aZz1y_iy=!O z%ONWuYa#0(zd$xac0hiE?1Suw9Dy8#oP?Z$T!37JT!UPP+=bkOJcGQ3yn(!p0mVec zfMcR#;$vVj2{DN=s2FriN(?535JQY1#V}%+G5i>Dj65bkrZ}cNMi)~V!9nQ8=#w^TcBH^+n_t4yP&(Fd!PrP2cd_choOH$k3&yD z&qB{ZuRw1=??7Kd-#|Y=KgNP%(XlD9>{w2$DAp8P6KjpFjjfM$#s*?Tv8}P;*v{Cl z*a5KvV+Y0d#P-Gxj~x*^I(Ay@+}OpjD`MBjo{l{qdm;8>?3LIXu@7UP#y*RE9{VEp zP3+s)cX6P&s5o$3Y+PJid>kwe9*2lS#$n@darii5oFGmSmm60Ur;n?SYls^jH!5y) z+?cotag*a_$9)$!KW;(X_i;<(eu!Hcw<>OR+?u!zaU0_{#chq-7PmcaSKRKnJ#l;E z4#b^{yBc>l?(ewQ@u2v)cwxLWUKTHp&x+59FN!aZ*Tq-FSH+v+&GEJIb@8@%d%QE= z74MD@#5ctU}?~gwee=Pow_>=KB<8Q_P z6aN|p!00eGj05As1TYCK3ziF0!!)o$SP`rorh`?$s$eFV8D@di!o08$tP3_6_7&_0 z*b3N6*ecjs*hbi|u-&jdu)VN-utTuJup_YJuoJM8uye5UunVw@u&c0ZuFe+z$yh(93e+25E+O}L=K_=(TeCmbRzmA1|S9^ zh9P;vK*;H8j)2<6S5XrhqNJ`NEb4IY(fT+ZOC?HA7npdf8+qnssg1)nNb#0HOh{v zN4ZfRR1npSYC-iub)Y&?15g7|gHXMw;iwU)v8ZvV$*3u)nW$N)*{JVO^HB>>OHn_d zenhQCtwH^S+JyQUwH@^Imv6>LltE>NM&i>MztK)D6^4)P2+g)KkC%}l){vvlweA8 zN=r&xO5cY)RRg@>|NzlwB$NQ+`i5l5#ZVM9RsO zQz_?DE~H#cxtek<<$B7Ul)EYSQXZ!~N%=eFWy-6Re^TD3d`S6-iN-)MF&G#o0fWFG zF(?cUgU6&|$QTNSieX{c7!F2&5n@CbIYxoWz+_``Fu9m~OaZ14Q-&$WRAP*nYD^8r zimAsmU>q0^#*6V`nlUYy5T*~N1JjA=j~Rd&h#7|I!SrHAVMb%dU?yU|#!SO}gPD$* zgPDu@7PAnu2(uXTBW4+9Ipz}PI_3uEF6JKQKIRGLZ_HE7E6hKb_m~e@Fg6+s!Ny}@ z*aR#Bi^QU^I4mBUiX~$ySSFT*C?}PR35bPQ}i| z&ce>aeurI*U4s1{yBxa$yArz=yAHb^`wMn6b_;d~_BZTa>^|%v>|yK?>~ZV~>`Cl7 z?0M`3>=o=)>^1Cd>>cbw>?7~ri3>>KP`>^mF?7li}kVsUY}cw90Lj!VH|aA`OK zj)h}Tc3dA^UtB+2f7}4vP~0$F4{kJWJnkpl2HZy6CfpX>4%~Iz9o${qJ=|m5 z6Wj~jOWa%BJ3I&G$}@4^qj55y0{_uxn3N8!igC*Y^xr{ZVg zXW{4Jzr!!aFTpRvFUPOL|BU|yzZt&;zXSgpekXnxen0+q`~mzy{4x9=_&@Q-@n`U7 z@#pa8@s~k6APXUfAg_=bWGkdEJ>z6ht zZE#vo+LW}ZX-m?+Pg|C@Gi`s`y|lm6o~AuZ`zP%^A(N0p$R(%=1%yIE5uuo%BUBLd z1OvfLun?*VHiDf{PjC_31P`H!5F|7c+6e80K7_u6euVynA%vlXVT2ySNWv(>Xu=r6 zM8emENrZ0*(+M*Oa|z!P<`EVV788~bmJyZ{RuEPa))Lkc))RgqY$j|W>>&I`*h$z$ z*iZPKaDZ@>aE$N=;S}LC;SAv{;V;4^!ezn@!cD?0!hOO6!b8GS!ZX5i!fV1C!dt>S zB8V781QTP4am09HG7(Nh5HUn75l18vNklS{L1YqHL>`e(6cD9E8BtDD5>>=(Vh&MD z%qJEQ3yEdKa-xn{L98O0h-P9P(MGfrokSPWP4p0(h(TgAv4z-9>_hAzb`tv&2M`Am z2N8RSy~N?f5yUaXvBYu2NyN#-Da0AXnZ#Md*~IUN^N9xNEi~9gd-72Bodj#ATdcS5}U*) z2}nYcj3g&1NGeh`DTkCx$|n_&3Q1+8a*~c@Bvp}2Br~a&R7bLr>?9}2MRJooq$X03 z)J$q2wUhdgI!K+Q{-gn?>6jGRD5kdb5*8BNBM zQ^{#$3Yki#kvU{8Ii1WSi^&qQl$=STE+Q9`OUM;uJ=s96Bv+Gb$X0SK z*+Fh3JIOw>pBx~!lEdUSayPjzxgU85c_?`pc_euhc{F(<`D^kd@?`Q1@=Wq9@@(>V zzVASoycj)JG8Qpgkvg-Kyi*c3iRKoL?z6gfpf$)IFXawxeJ zHKl-3NGYR~Q*;y~rHW#rm?^cCI*N_bNO4kJ6h9?EX`%!vVM-gNozhL|OX)`$Oc_EM zN*O`Z7?4j(X?4#_b9Htzh9HktioTQwhoTgl$T%`O(xkkB8xk0%}xktHAc|iG_ z@|5z7@(<-TSz zDOE<5QI#A=IJNVbrgvBdMdPqp1_96RBTQCsDtlPN&YG&ZT}!okv|nT})j< z{hoS>dYyWMdXsvKdY}4$`jGmF`i%OV`hxm~`j+~R2BJmLz_e&u94(#(qb1M~G$aj0 zL(}lIR9YI1K%>%VG&+qzV;Jers$p-E|(v@DvErlM(Rc{D98pH@OErIpbPv`U(h zR!ys+S!wmO2AYHBp?PUOnxED}3(;C>9kfnb7i|D-AZ-w>ht^9QP8&@dLmNvQN1H^O zOq)WRL7PdNMVm+ajy9jRg!VmcDQyL9C2bXLHElg@18pO13vDZH8*L|T7i~9f5A6W$ zAng$CFzrv;aoP#mN!mHudD;cq71~wWHQH_39ok*mBiduy6WR;fOWG^iJKB5N2RfJ@ zO^489=rDQ$J&}&2qv&Wlo}Nliqf_WqI*rbzbLd=pI$cB;(Y-4O^>|*R@>|yL>9Aq3~ z9A+G09A}(hoMfD1oM&8MTwz>gTw`2k++o~h++#duJYoFJc*%Ih_=oYH@qzJ?8O?++ zW0+880yB}B#7t(QnJG*R6U!tpiA)lc&SWr|%ycG?$!7|fQl^Y4XDXN~W;Qd2naj*) z7BCB$Ma*)hj#v&_GkmzbBCSC}`M zx0tt?510>`kC@Mx&zUcnFPU$d@0jnIA6Q^kGz-FtVZm4ltV9-)g<_#ucvdPajYVNm zSu_@##bI$-=`0aT%#yG&SedLWRxV4;(y;PaMXX|039FQ)XBk+PEDNieRl~Bg>RAme zH_OBFvVyE;Rtqb{>ci?_b+Wox16TuDgII%Ey{zG^5v;FRV_D-^<5?3}Q&>}3(^#`u zvsrUk^H~d63t5X;Kd^peEn}@_tzrGd+Q{0(`kA$jwVm}VYd32TYcFdb>k#WO>j>*O z>jdj0>m2Jm>jLWv>niIS>o)5S>n`ga>oMyI>u=Ue)+^ROtoN)BtdDGf4PnQyq3l?8 zB0Gtl%!ad5*cdjJjbjtpBsQ5%VYAtMwty{Ui`a6uf}O$6WaqGR*=n|iUC1tC7qd&) z2DXJ=&8}fv+4bxOwu9Zs_Og9!KRdu~Wrx{q>~?lP_8|5!_C)p+_H_0P_8j&K_Dc4z z>>cdg>E;aN4B`yt4B-stjNp958Oa&P8PA!(naG*Sna25sGo3SsGnex%XC7w}XEA39=X=g_ z&I-;-&MMA2&U(%U&PL7_&Q{Jg&UVf&&Th^g&R)(z&LPfW&JoUW&I!&*&MD4$&IQgz z&R?8soa>w$oSU3`oco*yoQIsJoM)WpoEMxooVT2JocG))E|?q5g>d7!Fm3`jk&EP_ zxM*$)HlC2)yc8kf#xaGBh6E|1IS3b<0Pj4S6VxGHWoH;0?c&F2)Uec5Xelf$QdaxL&T0+stj@hPbWV4sIv6i`&f|$Q{HT%pJlV&K<%1 ziaU}!jys+^fjf~ql{=054R<vb4cv{~ zE!?f#ZQSkLUEJN=J>0$AgWN;h!`vg>^V|#Ei`>7s*SOcYH@G*s_qg}D z54aDxPr1*y&$%zSZ@6!{@3`;Nqte0Y(dm%%_;gr$LV994G98tUPEScsO;1ZFq!ZI= z>GX6)Ix{^zotMr}7o}*Ocz#}h*Tie(g?Vkfc3wBHFRvf3KW_+cC~p|Ahc}WpiZ_}!hBuM-HE$AcGH*I> z25%nP;CuL9zK`$cx9~&!R(_b@$?xKK^ZW7#@dxvV@Q3q9 z@W0}Z<&Wc!=TG2I;ZNmHC;vGA1ph4m9REE3GXDzyD*qb)HvbO)F8>k# zG5-nw1^*@g75^RoJ^zCMEQl6B1Tg}bAVH8QND`m~XhDhqBS;ev1VjNzKo>9sOaV*4 z6YvEBflwe5$OQ_4N{}tc5oiVZf&xLIpiEFM&V1QttV3?ps&?^`&7%dnh7%Lbjm?W4im?D@em?@Ygm@W8D zFki4h@V#KE;0M7LY$B&BninvhL9;t7xILBp+qPZ%7j@$rBE%@2=jzR!eU{GuvDlQ8ibWX zqp(_7BeV+Zg$+W7&?EE;eL}ymMHmvc3Oj_I!Y<(e;XvUaVUMs^I9xbdI7T>DI8HcO zI7K*BI88WPI7c{F_^oiEaFKAaaEWlaaD{NCaFuYqaD#B8aI0{eaJz7~aF1}WaG&t7 z@QCoJ@R;zF@U-xZ@T~BX@UrlV@Rsnl@Q(12@UifT@TKsT@SX6z2rP;gK}0Z7f+$gh z5}`#YqBIdfL==%k3=vbr67fX>kx(QTDMT5f98s=FEh-chiHb!fBE85UsuWd=YD891 zgUBIj6!}DcQ9#rx3X9rA?V`S-exm-O0it1|9#OAov}lZItZ0&GvS^BEs%VyIwrGxM zu4tiXk!Z1KiD;VQL;{l}ND?I|30jgWNs~||Gzne8m845}5{X1Akx7&il_Xn|BgvN( zND3uI5}l+%qL&yX7D=_FMp7?nkT@h>iBIB}1SGAJu%u1WF6k@jC+RO4AQ>jOXf)CN)}2MNft|%NtR1iNY+Z$N!Cj?NVZ6}O14RMNp?&2 zNDfL4Ne)YnNKQyjN=`{mOD;kwbgguq^cU%7=@#j4(w)*>(%+>Aqz9#k zq<>0}OHW8oO3zC#NH0qNl3thIklvKul0J|=ls=L^mcEd_l)jR_mwu3bltE-MGN>$8 zmLyA-!DR>;R)&+|WvMcXj4Gqa=rXP>UB;8~Wm1_;CYLE>*|Hp2uB<>-C@YfbWEC>K z%q+9Ws%3Ury{tjzka=Z3nO_!?waUV>E?Kv%udJVJh-|2An5;)ON;XbcsP_{_6ShhsAT(&~CQnpIAUbaEDQMO68O}1V3t89mCuWX-ezwC(Y zsO*^RlO!kX6s~NX4ZfD%dc$o1h<8j85j29U%GhSu9%XpvhAu}p7CNn-0mr2N^WKuJk znVd{PraDuTS(RzZtjP>zwr7sa9G|%|b5-WC%s(sr3nX=4TmaMug zTb4b`ndQpzX9cpFvVvLRthTJKtnRG7Sp&0%W)07pob^rCtgP8t^RgCXEzSBNYem-T ztW8-zXZ@P>XV&qoTUocW?knL+v@%7BQKl-XN{&*X6e>kZu~MPTP-ZH#l(|Z^QlrdM z7AcFBCCXA|z0#$0D?Q34Wl-6yY*Dr=`zSk<{gnNcLzF|6Ba~k$M=HlD$15i&Cn~2Z zrzyWtPFK!R&Q*S^T&P^6{6YDna+Pwma*c9>a-(vS@@M5X<#y$-${or*%Du{c%Kgg2 z$|K67$`i_y%InHI%Dc*Y%E!tl%D%5S#$=PSS=qwujBIswake45Cfkwi&kkqz%^sRPD*Nl~8QI@uFU?+^y(#{bbbLw*%a$Gs1az^Kj&zX=j zF=tB7tep8dOLD%?S(@`h&dQusIjeKl>oEJGSbKd5>%LU~|vFf`?#exwdp!4i?zP-|xleQ7sH4>J>LfKmO;Iz{Of^@nQfI66YJ=LO?x!B4 z9;_as?p2RfPgYM?&rr`)&r;7*f2W?WUZDP7y;S{!`bYID^=kDR^-t=b)!WqD)xWBD zsQ0M%s`sh)s}HM>sE?|TsZXj;sZXoVsBfzus2{2ysh_Ezt6!_%s6T1|4MY>8foT#n z2n|w$)!;Nl4M{`RFf>dJOT*UiH3E%LBhtt<3QdM4Q>Ov z&2-HP%>~VM&3(-?&D%V19xM-)mzGD**9x>ktyHVj=4p$x#o7{W zsaCHwXe+fwZMC*WYt`0j8?+8>qt>YnXhYgoZCKl;?b3E@`)d1X2Wy9DhiZpuztWD> zj?#|SPSMWL&eYD*&endXov&S>U8r5E{XzSqc9nLucD;6kcC&VicB}R`?N043?QZQs z?J?~i+CR0&wP&RKO%p8{X3$ieHw*mBg12N{A)Y5=n`oL|LLP z(Ujzs6qXc~l$BJJI7%8zI!ZcA`jw0>8DBD~hn0>f9aB2CbYkh(rBh3%mo6>+p>%WUmeOBKkCq-UJzaXC^kV6i(yOI6OJA10 zDvK+NFH0&TmC?#rWxO(enYc_+rYOrQD=RB6t1oLPbCvZe>n@#9W6UvcB1S|+4-{jWe>_elzl7*m!rzDjvls>W1k?=zi2K(=FGn)~(a+(H+no)E(3P zp*y8Jt-GMRsJp7Wrn{}XqkE`(q=STpjR*|I2HT~afPHpQK78JsmQG;swl3|RX8ddEBaRSs~A+Vuwqfg(ux%o z_bZ-M{9W<1;#tK%6|XDaRJ_##dXPR!57x)(#Z2=#hGq9;e6aQ}tFIioo~uvS^Ymi9L@(9L^jUhPUZv00=jpZje0_nwR9~hq*X#5~eU;v%H|uNlb$XlL zu6OEPdbi%AZ_)?#&H5I7yS|UUL*J?IuOFZvs2`;7(f8_y>qqFv=*Q~E>Bs9Q>!;|a z>Zj>v>1XTb=;!Je=oji2=@;vN)GyO7*RRn3q+hFFr(duCMZa0UMZZ=5n|`N$mwvbY zcl`nVLH!~9ANoJ_$Mq-lXZ7dw=k*u#SM*o)*Ywx*cl3Al_w@JmPxOE5pX#6K|IxqJ zztO)n00xjD$^bUR8sZG`2ABbEKp2n)lmTbJ8&VBv28w}dpc&`}j)7}PH}DK%gTx>; z$P8Hqr9oxLHsl$!hI~VTq0~@jC^zT~MnjdsWH1|Q4Rr>a!ESIGTn4wnV`wr24b6rY zL%X4mp~KK==x-Qc7-$$|=rQyfh8sp0#u&yL#u>&NCL5+0rW&RhW*KH1<{0K078n*9 z78w>Bel#pIEH|t${A5^bSZ7#o_{Ff4NnYz8=e|o8U8W6HoP%>tOQp=D`P7YDw8W|mGnwZ zrLa;|DX%QA)Kyki`YQvKEtMlHCsb~z+*rA}a!cjb%59Z5DsNWat-M!xzw&`G#fUMc z8q|EH;)HON|vqqtRvb8w18BW2-T2Y%_KlyN!K~{fvW+LySX>!;D`UM;b>N zM;j*?CmO#tPBMODoNk<9oN4^lIM4W~<6h%I<00c=;}PSZ##6>~#`DHY#;eAg#yiHl#=nhEjsH}^suHRYRhTM% zm7q#eRa8}8)n3)7s=I1%)sU*;RU@jtsv29htZG%&>Z&zW>#Md@?X22YwZH23ssmL= ztBzIuQT1ol>8dkTXRFRtU8=fVb*1WR)t#z`RgbD3S3RkEQT4LwRn9Faf>7wa|>4E9F z>76;+oM1+q31)_wZ zmg|=LmS>i?)!=GaHL5zTnqJMTmQ`n07gX!2E31vwmTGHtQ+2R9T-{kcx_V6Ygz5#= z-&gOeK3ILI`f&A;>c6V5R^P0?SAD#Yq|ht*^CT76c(wZ$5; zwpzp1PHUI7+d9xX$l7D=wGOwAwvMrmwT`n+vQD;6u}-zlu+Firw63!5u>NK}ZarbW zX}x9r$NIV!R+~^uswLM-YNfS>wMDho+S=OY+LqcOwL@zs*G{QjT)U)pQ|-^Sf7M>9 zy;gg@_D1c^+K07|Y9H4=seM-as`j7SceNku;B|;POkG-?piWpPtE;Z7t8>=5>il(q zx~96}b<^us)~%~MRd>4XeBGtGhjmZup4C0CdtLXY?xPK`fow50s4do(XiKu8Y-k(a zmTF70QEXHj&BnHIYyz9mCbG$G3R{LP+m>U?wP|fdHoq-kYq53M#@Qy>CflalX4q!h zzO~J>eP>&2TVh*gTW(un`^mP}w$Apm?HAj2+po6Wwmr7Jwu81qw!^kRZO3hAY-erf zY?o}8ZC7kJZMSUqZ4Yb@ZBK2_Y|m}4ZEtKJ?SLI*kFi7TvGzoJk{xA7+lh9Xoo;8? zx%PBB&n~u0>{7eTo@H0sRd$U%&#tu>+e_@F_A+BADqupuu+5Pr_y~*Bc z58K=9UG{E!U;7~YVEYjJQ2S{61p7q$*Y;`lZ|u|UbL?~N-`eNd7ugrvm)Mutm)lp^ zf3mN&ud}bWZ?XSsziPi`zh%E?e`$Yfe`kMRA5{;okFJlakFST-C)6YAk@cv0bUnU4 zwLYz$Qcta?)wAn4_1yaOdQrW&UQ(Y?pIM((pHrV#Z?5;(52&9|Kfium{oeYs_4n&P zG$0xn4Vew)4Gj&h26uzMA=ogmVNgR)!^no24YL~NHSB2E-LR)&Z^QkD#|=*!{%&~D z@Xi5t#5&>}NseR(+L7W&b)-3H4z`2i5IBSmnM3YSI#iBwN2SB)sCLvi?2dYe%i(qe z98Hc^N7&Kn=yD8j40H^0^f*R3MmfeiCOD=zraERiW;qr*mO6fL{ODNaSnXKj_{p)+ zvB~kX;}^$v$FGhZj^7-69s3;n9fuu9948%T9Ty#cIj%ZxI9@njI^H${jmSn+Bd$@^ zsBSE3EN;{_Ry67x-HranL5;&2ziFJ_IJ!BsI-EYI-`V19clL4ibM`t%I>$SwI;S~jIp;YSIhQ+EIM+Bg zI5#>sJNG${IFC9{IL|vTIIlQwJMTCjJD)gTINv$nyP{liu6S3n3**AN2rjyd;o`bf zF0CuyRpu&p>0L&b*=2RvT@IJe<#z>KO|GD85Wsg021KqQu2HUWuGy~5z(dz|*G}L^ z*Iw7}uEVZlu9L3IuIsMbuKUteu1Bt?u9vPiuD7mtZonPwj&;Ym6Wnk&+MVWRxS4Ji zaL3J-9&@L=#csJxoh3^Zl~Mh_Pbl%ecavd0q!C0UiV1%DEDahc=sgtH1|yR zT=ydPkM3pekweF4Xt?r}l+aj``|ii?XYN<-x9)fD_a2Z3;)(Ml zdJrC*hvXr9C?2YZ?csSOo}ao^zhRJXbw8J$F41JN$9q1kE?e%`;9q*mw zo#vhCo$HiF`7j(pTX#`Yb-H&*pRae7;}=Ve8}Tj?i}leS>_% zd?S3LedB#oeA9h%eXD)ze4BimecODyefxcfe8+qzd}n+Yd>4Ix`L6nI`tJH3`kwh- z`QH2CezYIwPxF)fOh4Bz@Jsv(ztW%USNk>o0)L5L=dbiv`|JFT{tka%|3Lo`e~*8( zf4qN^f0}=$f3APNe}R9Y|9k&3|0@4l|Ihxd{+<3k{@?w_{MWKB`fvH~`5*e9_+RIFWZ93X?yyCa#liBRF=z?ef~~i`!)}39@^a7{8jUq=C7NlHqUBaQQXqJrg?qyrsmDf+naYc?{7ZR ze5LtD^PT4V&5xR&HviN7t|h7^B?H%jZy~l&TIen87D0=oC9|bGdVPzb#ne*WQrlAB z;%@P`gj$BRjA$9%GOlG}%aoRxEpuBIwrptmrDa>oj+R|5`&tgS{Lyl{x?ZipWehcZH{P+rI!vWDy-N5~cOg<3)p3NbV$G$AxOG%YkE zG$*tmv?R1Vv^}&lv^Vs7=uqfb=v3%j=yK?3=w;|l=zS~D8r>S-n$(JJWw-KLMXl0S zMXRb+-&)mL-CEmfZ*7e5oWa(%*6!8;t;1SJw2o<=ThZJ)zjblz($;0IYg*U0Zf-qX zVQc-P^p-$fbN`y!CwYtI-eJ~XAb=dxWEGX0-2W*i@?SR`BiS%C_cqvd1 zGyo2u5pV)oJ#oG8oxLb3}*XJ$xpL+Ozh}ZvCC4T~agRanjYAl~} zQQtS{>u2_LeWF}Dd*Q$Jzt88 z3zKFd3KvOa-O^-vHAibDt5(#7tloFdLWy%muy$ z<^kUU^MM7xLSRuOE7`!}$p4ZE&72+~n|}nBMF{8Rz>3JzkCFOS!0JftPm#Q>1J*~% z8zR;lflZN?pMhT@?_yv}BRnJ_EJEfuDI?^Jhr7t)srv6YT74>*)MK??#yB z0d3t~dRHLq>oT?VEA<5fO>B#**HI;sUWMOM-+@G2|tGhcR z26Y{6ZCxc1hxQ1X{9n`0NOU8d*e|jRJ3lehzOK*x{hz7*|8!pG@P$8x=lwV6I^Sn( zJ3?|d!mR>;5+~-2b<;FDb13 zWMmEwj7fk2ZVmrCAl&0>H+@RDv8^-siJGqaqU=biJ_2nA2S(Q5%Q_(^-oK@4b7T`s zPXOD2Un5fA5uuxZvl`6SreK#Za=3j?ojC&A`|>3~Px6;jY!|RQV)D6Z5wHs||Hpan zC+8xuP^yTSUWOBa-+=?bLEuoN-^0KW;Amt!9RvOV{sfK#CxDZ{Dd0441~?0x1I_~% zfQ!Iiz$M@^a0R#uTm!BHHzM2SN8nauliUIB0{0?iZshR!F|xHD0*@l^Pa>ZWBcFFZ zZ=_Gx4t%g20cMYgDZx&i&(|JVOzmf9ZC9bs@9GZy&l3LIXdoUac=~HC2b&*iR-p_#k|9A((wY9;{(xBJt3;);0)xWOcKPI20`$br;PsegZ z-=FAxONT4m>2K?3?JNsNc9cJ|x;?m_-y*`<8%x(b`J#}XlpbUcDvq8P`t5TC{kMM$ zaWqX20K~;cUU9MYUk-7~=Tx5n-hBSHc?-M)-bWN7Z1D}~T(F=A{Y5q(K|n-Ng+b4! z-4nSCVG;20lXilUb5{q70qapHrTNb(XU@<5b6bG39 zS51S_Sn=gvg-`9DzZ*a>z`X>N07?WUfs%XhJykvZdgk`*xD3w*AtT~D2||I;z#pI# z;1UP}90p-QI1oM}zH1Rt{uPnnAy6uC8k7bi04G315D7#EQGg(b3ZemLLG*}BuYef9 zWe_tGFAKy5aeyNrE+`$u1I~c>AOT1S5=Eqa4mbr8gCrm+NCuJvcR?ATOi)%tq}=}q z6_f+YjY#?qAO__{M4Ju#2>Ns%Ms}nw9aJ2-Tp&^cmHt2W-UB|W;(H(7+qR}`&x$B$ z6a*n|FCY>+C_9jESWvKH$NqoL+}&ie z*&qVJ&-;77L1A-ucV^C+GtVh=&J5mJA{~v_tXa0_eAHqdVcBs;NVv zI;uKVP7$^S4tL1@P`^{A01*MSU9-N~2MttC;A0?Q0Gw1+QOTG*saxfg!DC<(q>)zi z-ec0Fn%-U9aQefBP8r5MA3b^eq;%KC)2n7r&l+(*{v8So3vv9ZXu5JjhY2{0f=~H? z5}AorI36pb`Weox;PGUrb&lg>F|tMo%&j%z3VfjI_*vs8jmDDgGh;&4@y6SNf*})u zep7qGD?ndjV6}Wu zDl=0ZCXKBgTUryjY7x@8a@x3l%=>;*Dp@h+&a=_#ApV4*;yl%(?_SN?Vf@_+Ovd!i zmD4MAUupTE_32eV<~8v`KNRb|v|PM~;yFV1S@r#`dNZ5uv+2zN7LD+3!g$+e#k-~D zPUAPd0`DH6A5vQG+QCoc(lrBg@s@sAX?gow#LLn(>F?^{((;x+@-w-#{4nvJN$)zK zW7VnStEQ#_Mj9@SkSYNzjSZX_B>~wyz4{(IV>%lWH3)N?G>S3iZLHTZ(pYI6PO-U2 z8jlryqI42g)8`5-XhUp1uCI&N0nvDf~ zifN6|uoI_Do`n9U_3zcUPq&T`RCFgSIWYpEe0w_&gIY}_qmfV~5soztmLW$fnG8o_ z;S_#JC6M_g9yAU0b~eXHL#ae08H~iEcsCl1M}whwBpT+A#*@ipC=rfFOnOtj_;u+t zX)Z=|x^#x5_v_WSM`w(pN2g9si++%PaBo*Zcay20J50*SGNyAPtbPo!>7ce%xKd-m$UeUB^=IJi}>l!s> z>U5I{_-8UjaNN!)yq<^G6J|}H!p~V_c}JeauVs~APsIT7I>4{T8n0W4&ls=E`Ss|D zlSbn+Jmx8*CywUVPviA@GiOxcwbB!>&tabguQ%fLAut8U<28%0cVboLG?Pg!;OEn; zMvcSkAYKPSqsKyy&jqMnpYn_Yw9$4$|+OXo@{#9s1{8_sZ_FQ_o`Vi zny0s};IoaUj)qewj`J~@44U~{CO7l8DVQ7&rQ-3{;WDtl`e&zaR)1>eFL-W^PyBG4 zm@~=0zUEoYcWa(aUT88Ue*$kVt$8-`T9fIavrVSJJ2lS^xy58Eo@X+x+>k|&pXFuT z^yyRD9C5^~S+mNr$&YTh+X8);3MsiWZE&zRbD8e6+4 zV$2cZ-Tq?Mu(HO{s!!FJD!>f78UQJS62f#t8L@5Dbo`|HW_UNhmNf=`X~;ERcozL- z@(wREIX8Gs%Gc{ns>fkchTepCr0RL=X&ZpZg~>Fq$>)aL<2BwFf7kj;z68I@)5ec& z%8xts>DP4BjHxsEHP!_GrQxKpVw^0tA5Ic0#mQl*f~@-$XHSw0mu zSZb}dRlBSG)lurn>gno*>UHW8b*1{W`Z|tL+OGbk6>5#O!?iYA4;+^?R-2`rgM*P4 zYY%GA;P9g@+RtV)4mN6TPMEu!2b)hc&oN(UzR|qQyvF>h`4jU`7K>#+%i)%Gmfkp! zXgZD_y4G^HWi^fx+G5#Zby%BN!`80WAvh-JOzQ&cUDhY8uUS90{*J?b4z;zh^|p<+ zonpJhw#c^1w!yZ=_PgC>Z*FgA?`J>JPWEf;_u8Mazi0o!;cy)6NZ}Bj@ebW_jbpjv zc^rxJYk|9YYN^j_@S_{uo(`g8B#dC@Z!Qd3!f_dpm0Z#r>Lx` zN73k_Gm5S+T3Pg3(Kp4m;zMy9%n8M(7B47%pm;;^S2(__n1 z!x_p0Re|||Wq~&Wzu{nnF8iH?Lk%9<@BKzaTD76AI|Nq4fP)6s&)R^7L|)lwWz^69;Z<;z5D-_$F62S>5D| zroN^fnoekX4T_exqtLiJN`r4b=;eccKe#!HaL+t=*}?CZ+EKtdru53vwWZq+X@WA* z(+*jB$UBGH4ox09?$E1I2Kft076&#v7X^b`QQFs|`Kc(udk-ad?GHN{MRFTkm|G-K zVs>MTS5VRwYdNmv^({9XE~DgXJPM;;ZKbtJp}c5et4*y7P@prj_0rZKp}eIhN>Cmu z`vzqoLr}8u+!22TV<^E`6nr=24jqkBg;k;N!!5(3!qZ8 z(YIor*s(~#UK{%}o`_FHvh=pZA&C)*8xot6-eg7c!sPQQE!8n~29kMyZ4+-Zz0Lh? zzHQsO?MZF#YPwZl4%erssaX^nzJ?`wW{pbWzx}H9!0LfU_9rJO|mOZETT!r+eqmfYbUav!Y zP3rYfZ&U9cy|3u~ens<&lPexQ&U{?Oan~RBS)Wj!)A~Hu*VA`+-@E$$+^=K5OZvUr z|FHfu`mY^OJYeX6I|uwSurs#lHxD`jTkkIpZZvrO;D@m-K48e=AwM181slU#hQ@}T zGxW`2&4O?!B{7sk~3 z8Hdd{d&WmIkD7V?%pJ1^%z9w9Yxbns>rZKU%K4{!ernfKZ=a*i88v6^X$PG~r+qZH z!`xd>mrfsf`r0!}&zN_{7QLIkgzR(@y>w>q%*)UG@vMPot(tehJbm6LXLmb$={ZH` zOg(4QxoywA`8@T!@#npKe&qaX&;M)w==m>P5WHZ)1%F&P>cST;3SD%~MSovB=Hizx ziC=QlrRGZ~UHax_?Jrw$x$E*%FaP9?MmAiCh@tL<0MxO($7 zJ+FEA+C#3r^x8kJ8+Y9s*LS>r*$s_vIRA!UZXA8%Yd4kObnndv+Ypwet7W1uRe10BWqW+UG?CjWslzR*g=n7|G4k*OP;VlG4F}LSD(82 zr!`a7eD&nmCqG#`eC@kW4SH(h)5ksi@-xRg^Zc`2pMC1N4$rNAzTNYWt!uOHkr$FL zJoIAx#g*%0>mPh6_R@nd$6sE#A+h1%S5mL6diBUxpLnhPYfrx3`SoWv_SpF18@=9m z_09fozO`xSrp<4SerxO76W;#rotf|a{;vL>_TK#Wo$oLBpz#L_KWy>gvd!_$kAKwp zqxB#6{rKHaMt$<-r_(4Tpq{`~td^LOmGW67_{ zU!VJJ;BTM*KI;$5AJ_cZ;?Gro9rM?Fe^31TZ^KnPOPW_Js<==(+tjz#6XSE|O6Lu% zs1Od&ZPK~Y`KG?LgFVBYhs_C>g`@FcDw2o=jh|Gjrgfjq9qw#72X=DZ&*phJcfg!r ztER!Z6>mwGNS8{N!85vCy28|0x>C9dMoCb*M!HtI4!+y<113%D3YU7wG;tzlFW$iz zkN7$t&&kh)zs*mFjH#SBeu6lXa}WpG_?g~HH(B*E{cyd7-tvU*RTE}baTK9xFKiGG z>{B&%#)QhL!>S+i(~Z(Cuv8XeILAx3VFoeY*8RWVuT@j zAf`Ee{22KBh(KjN*=fS$X;q^USD8Lzn%+f^IL&$|BkH%-M7K&$=~2C-@%ytTS|mLu zJuj`3UeIHDN^h$lrFVP-V|Yn=S=u1I0$Z=6^g1Zqh{ypV!MyDR`x_CX2_g)FUNHa> zoX>td1o7VVxCTv0?*r&by_p`@6NX486VX)lPcoHk)ihcb357Ev3PDXJ=MH5lYn>BiOy?s1qLZ<0oD7|CqoBben>>a`B4@K9_^fo8V3B}5i=mo8YBXeti_7g-G z%cU*&?2#B~yW!3jb5JjqJEpwjL@a7fBvcj`4X1}&yYVliFHI5YD>PoNx7Rx$1U4sD zR=ps_vW!M~MGbcb=CscL*3g>K2h-g6PWt|Y>Ud6vsZ%RYxlQ^``oYu}QDyO~G0i{m zwEwJk);rn^F8(I{4)JC#E|h-b;IarvOMl9;RJlm{OZr1J-|`TqGCEPT7TpZ~Q68zxW@)PwWGT&_9RwrdCcE1V&{Z zbxDg2o>hOWiy>fxvmT|tIe5tI>JVn0Mg|W#CF||n_6F%a5p=5lBUm8sCpSV6u%g0< zm{kiD$MqJ<`eIJA?kKHCMV#PrG!>s5)mz(GH(1RtlEl- zJ~&{xa*_bi^y9%573o+1;XZ(AiF~Bo4&E;B18__~ShEixcgF8|_W|UdcxQ>+EB&3E zeE@lYsf9c+dmq5`wLDlJA|uZc-x`WQ?wT%_vg>Wq`(ww(2;26}4%_xYptkod;v*t# zn~v7@tB%$pNV_Ke0ewIwHY@3q1{ng@6kCU4k;%j5bWpaTbaR#pk;lLl9|s*U&}r3; zogeu`fPs9HJ}9jNqB(xp8%{PNru!KG*8A294ncV|D7RoLZQ3>+)*Y>)0qOSPtb=801gAMYr$j!whO9w zYm6CSXB4REo)Zp5g`xnq$0OKVtqt$;xeV{}`Dnco@IKOLJz18mhEgdEJzVD*S}&dx zj<70M zr;isRQ)hsA2R%&a&in(u5xiI--;7pI0xz&310yk)(a=VD8QnOeg$!Gg{|;BPT&NrQ zwrX{gk@-91d!@?7X~nWsmgIZV%4M>Cv_56Ayi8s$-zVQMKOnErPu8dE)AZ^341MM< zDw_WSZTAhP??x~GA#}@I6-K>_0s_6KZs}g zl^HOPqy<|kREmrpHip6)=Lu`4+K~c$G|G0Y*!HY>GYhO0uRK=q0j%fhSBQnAlwct# z0sZteR4e-fs+Gpd0m^|&6Q!wgkbZ`Krhc}5zJ8&8Ne)y?9hGLLwn}q>9J)?OIGPg< zmesPbgS^H=3{tmc*l!KtEPrwo%J*Z zx;+`J0L`(Wu%&Zak0?=7xe`O`=j!L^=Lv;U2a16CY#5i+1*!Bqo;k@-89TVlTSGh4 z?g!jB(iBnJ32w~SFA(UTOGXlYaouO=*gYo{O_l`>^F9`W-CnzOr6ZiANlIt5ei3+d zv7uY?F*{tA%{DL&tsgiCdydd@S-b;Tts^l(QBF@il%q{6lw;7-W%{N1<;PdM9YHut ztusD|!7YO)HO)pbP3$-z(NU?$g=nRpa{SI9S{bSgQ%+EZDOX{NOzHS=WID9Zs5_tk>v2b2}c zgUU()(GM|1S7ac%(rrNW%}A3l%*gQkcNl&(pC zQop6f-hMa((a{V$2Eo(UNI&CHFE4-BQRp%c@9FnE%5qX zGL6)p8Qz0?@8a0rA_}>bk{tnIG2j<*w zsDWK$IbsrXQVC!`4_jb+4TegQP^K-&-MEA*ADuR<7lE%zm@uiypL_ZD0SW=W}G<>rea-w*NhB8=W?yJ|1BH-I)@=57#~ ztjO0xZm9mWRbOk>pR$!c-c-P$37My>g`JAjXa4Eml@of?FsKezhZry@$WVu=Ct%}1 zd}0xP8O}kXdcs1+7b?|}_=3n^U8Gj3qjvfQm>6o6I`&qn0J){l84ZkAPejfXN*5NY z9W@jGsM-Uvu6dQtj3o-JYt>fgy@L5>aZAP92|G(W~1c^)z*^ zdO8m!-`mJy&izuy@zW<(BAMTr`-ahzt`b6TMn4Nx-PCvQ*H4JlZ3xY|rWWdXnfeL! z0FGAEctA0Yg4BEQ7enMY|BxI6W>#raWo}@CXz5xMIrrxhUzzOr}gW%vp>chP3)K%)EScH$KtsS;_P`9FQ)YpmOm6jJ?%Re_`oUfOb zA7v~)-GhyvE2cko;u$7u>FVv`VY|{by_Vm$$=+@X(!+aQjel?3R9fEk(zD!?uD%G5 z((T~-C|n@bUo0(y*k>74p3~o8RM`_^U#KCLs{_F4ng{VqGzo&5 zCuv@@LU8j2%QYw7S)#e|tsHY++t0LBYlP>kggL(z&H<9_kaSpoq@hpAXejMqt<=y^h~O@#0Z4(1Dr z?BOUU2qTv`7>%VO@lYfp-VTRi$QMo_p_o4#j>eLqC=SXL8@B`Iz%q=@9faLGsI=JJ ztiX;I*AiM1Uc;x(rX)2N@}srE)~nW5J5p<>9i?y4ztMjo$({Bhv=01cN3E0AS?i*I zrf=21(7&qVJ;2a!t8plx8UB3Ewk#qiATBG!fNj;*h|Gk7$*hMt(F>Twi128~r5oDD z4MjjX&M2(;;fC1iO$2M6rNXr>V9`*yHZa}Lm(?T6`brA^Olu4?9L|22$kR|n8*|Jffi#1;Bba}c+DL7bHd_By|4#ow|0y5;R%j=tJK0`uC((FWFcHmi%JT3J_wP(e zH}rkI{ELRlf}t$_B~x|D)@Ew6jE0a|{$ur+vYEvkWbY=P$Qo0;Zd17XXKs2-KUX)D z%fURv85~4a={*{0XKH6@^R%;(P(gzyNwU?;i&(rYnM`E~PMzq2)Dbshy`FB%Lz1K3wlLUWC_5CB%)_I% zwRh5Oc}XhdwnF(t2RnWVAZN`_J}KO+eUxs?M^Z7jl~h@=BaI+&zGb5^(Z6FB>ehTsQDUcH$! zK$1@cKq6(e^bx~92dABL!eQ(?umHqE*tMdw+K)p`yakK+ zemIiN%rM$3;kWpmSQfq_frv?uVzq^{^S6_Za&ytnii)6NNSenD?)0_%{ZCtXX6iYjf_7X)A0v$Yjas9 z{($I!IRx|B944vdUW%NUQ|#5?|J<7AwEG5wQ3Om#IvkhvvcqM~jI5KnbN0x(aj1bk z&F6S(<|Jc)yhJchfWq9fIw;X#p1!%CsfD?J#?vZq_(GTJ^^NauBU$iNg?6t zUkI%n%=r5Atw?LC_G#Fe={$a#I8=Dyn0m>x&fB6r0@j(9) z^ZNX5zxnl@yZz?(AqhEdzxmUAZom0+^H%dV!|gYJp~EaG&-nc$9r;g=KS}NKd;TPK zAnBY8_XgK{OwXgR2Njj<)qGiym+tjL4vNEpCJIudk0swI8 z6H%l*q~Ztxz>QBtB0=oYAVI$NFGhiIJQYgD;P!_jcsHKlRJ2Gi5jTEG;`W79G$q{s ze150LIrTbd_Ii%*uv5hg?+6^I+mo~BppXm-+X*rVF_n?>Q!$~c|7IXDc+m} zvf7f&v{X?qA9pPdU^yz&QlIKE3Kd*P;NH*_oCqcQFQFIX&|Y96rUhi?mJrH52v7@2>g%mUJYGq{(7T z|HkXL+zzkbve!@mrp-tTtOMYj^;sl5`45bLyw2mJeO8zi)Zg z@|@Xad7hg*ous)WozcMFzGZ{Ck>wSZCX$GxGi#0WpQga@^(~t+!iZeYI&m69DZH~Y zPv7#9<>M@W+Oh>Ev*k0A&SR5#FXTvAzJ~d`OP>DuIi9}d7t4<9k^K(;-Eb%2>IX9E z2zh;dtEmBf{q(7bR;#In)t2$~tp)7sTZ?M@`c`*sUmwmg$pMlsVP9X~55E4zXLVM)dTm9COY|q}>ILr6i=>c;jT}HCE+OxlOr=GpF1&$-LwzM8@ZDnmu(&Z!p zeQ-YIcmJuIWewrz2b6chVd6t5@uwzP(%PmzH*4odl395NWmbcpZ+=)g;|6FvL0x2_05gjMt<+A8RE* z#X8bD$~xLwMbfn--9XY!B;A^$bfk{f6HRTcCmA4j9ZA<41k}oz+ogiiTKY#gR4uI4 zw$_b1s};4*#FlBf73LI4H#64W!rFGPRf}5ZT2D7ctY>iZw~@4vq(!VZ@>hk56wLf@ z}s6{t{au0896x5dQrZbPwU0jORSe#FC*y=l9rHkCrNkx-%1+%PvbY%2d#X* zZJruO>*MtSi&5ifeGcEtiQh;#>(IIZ!178qu$aEaNj0xqHws{RBdf-dqAnoG zY=K>C{S0UJY_)E)eqsHRr29!)Nzx-EJx0f}{sXdQhyg z{Pm2yH1oexf2E^!2PW%R1Hv96>0tpM@UChl3kR`1mbV1&pK2Yge=$7S#QxbThNnl3 z_IIst@E@ujZJNz&ir6g7oF_HxxwyF6M-FB*}g>6nY zqTA-$PPd(5(;3lil&|!Wq*rU8eM2_1=P!OF>6Hw$=dFGu=`~=yQT|BM7io;Qon@Ps z!T3Ecd$i4$$J#Dn41b-ZZ34q>7hy5kE+%PX8pCau0mE&V+pe%(X}ij{zy>q&4U*m> z>0OdOBnfE0B?rT$jSzl3-DMMbg(KeM8c>ByIm6cK`e0n*R{H?GAf^y)Zv^+r9N+w^0*mKQKRb z+nbqM*qdi#x4nhErTuVwE5>en>wGnlB>h-}-QQhxlpUZ`5 z?6x0a5AHSWw#VhM_5@@1&m@v zJ4t_$ER(E}Y^{Uc_U@*(_8taI{Yuhr0&#b(POtvbNP8~^a(e|M#vhDMe{!?=stfBr zL+B&Bm_2WGp}oI-fT`S${Aa>0uk<&`NZaI6KT*G#mE*W4^^x`yOcDEV!5LX0Srq_V zcf7k>A!#4Y2yVwV7|CYvNVXUnV%K=j0pp$3NZL=bPcW^pPvq9^BqL&p1kAl(CTX9V zi|6*4_PIO5bNlJ`GwirQ$9|^$ERu^zE+*MYvWsLl$)5k=`M<|=0}ECn_HTcPWZzzj z{o9|YC-yI*vw-SMmY?iT=aFTy{Or%$*BP*9jBGvP`PP;QHFKKZe(5y7dMXj^uQq(} z-~KkB>77jQ-~K)a|Lq^v4*uIefwIX;^b^7V+evOLg8z%L6R>|K_`k>q{zvvG_;3H* zzBN1eZ|9wtT}$he4YmV5h-<`)>&T+kdzJVgJ((ozR42z{Bw*pZ=c){~fYJ zaj0plASd|maMWiN>^%7IXl!cXI3U|9a5RwyIhr~SVynP$aNgj*d~kK}Uv8Rh71R#? z%LiqQfm*?Txin(~K-XrDt#KUUKxpC?Tnr(iAPpP*cN{LQcC>!p9I#GEZpEK&EmY**Ix5o9%hB5uaj=yl2T2B#LVGFr@93Ya79E2fC*%wM zJBB+(I4T_@Nsf{nBNHT$A73aNGi5xiuSD zOkX>0b1ZV)E`VimcJN&lvBvQvAZ9JEzYZjqlZ>loV8!PT@$XLX-?0vp^@0ImokiEs^yW$L;i1J?QTT!cjGJnA)*%q3ql3q{H9_-s$N7F!T*9z`4PPUc_Ib1@^8`|p${>#HM(4If|FAuK?{>!t{7+=t@pnnGA_c-`p zaJ)RWU?^kw2$E+C$Sybmi>Y8Z$(3mgFBl07FBnxYx}d6HOu^WKaU{bssUjIKCXhUt z{v zJjqBVg|^A34)!?sUvQQwQXq12NuEgZBqOwwbC~092mcGs=g?8X1x(5mX4T1t;%SKB zf5D{%mzh=+T+V$>CwUslGj=!lUvPCUb{AYc5-^1L*57Zw4#3yTY#g|0$(p{LMG^4TPxNAd+EUqbTbBwtkry9@U-wJmI9 zVD~vBpDPe|*82mcG3Fpw7>#E5Y|qtkqDHecYp?z3G9{^QcF=BDz(!?=B1seU2J zI0kla2mcF?FhvT324^lM`7!~pb;rBA!T-V-BY0tgX}E%Ubfuvoc8&M{6#Oqdvap?L zMIp+XNWPln1tee75W)Y#PPuqq*sZYF&hWgjcVR{0afN*f`xf>i`FfIXAo)g;ZzB0- zk`W`@4P*5G=U*|f;9La%3$fG}?xo;=;U&8o{J$+H_+NN!;dKV=86&$9@qGL52sLx? zpU)x0@lH5eBL6vrg$o-#`d@e#uxV)~`d@f2NB;|#*N*-du7I}5iT*2LlJDf`zY?*M zyd)j{S3*YgKe|WJ|H6k0AIXmX7xLcAE=K>Cl2TS3{l9Cc(f`615&bV*U-(kt%Y_?A zzMJG{NZv|{<3El57rtJ&vG9%jW|4j%iR-U}{@bXehuHBqJ@CuQ%H( z(f=YZH_KY@p`Atlivk>8D}s4K@+17|RYFJZt>}MIlcJ`kNRcp89w!;$$|v?x^uGwl zv(%1J6}2onB46~sC|DFK3KvC4ev;(1BtJ#+)Bm@$;D6|En3fa`EE<#_Sc-<%2P{VP zzi51Zf1~JRQ;VXh*}!7@x@daQjG~zgEJd?wMgO0#!INjR@ua@!|8v>EQWO1OmjM<; z|369tOVKGsr|vUgDbnS!Ma01J0?C_&UN1U}fn^@aFJ^${TzDTv=M|k_G{5MAq6>>) zn6D@K6_Q^k`Aw4FA^H6rh$*_P=yE{J6}-G(Cix|jH|*u;e-W-lED~qek^CyjuNgs- zy%qg0x<$Yf%797U$nf-r(f+Q*(ElO&Uvx*&5`YtqxFh*3k~fk3_Fj$t7cG}26``7# z$NetJ?+J9@wUO^6`d_pXELl;+mBsQ0-1LW{-CU0Ee(HbGV|9>qP0@2Z^Eis0FIrdh zLeYyw>x&>^ACvqE$)A$Eh2+mj{`~(I*6tfu=s!gCVyRdzR`MfyvAteI7t#M>Z+=8C zMxsb@twfRHCdEyQ4=O&G5xuxHfAs&$8ff2^4ej+s|G&sUdp*(ruYmFC)PF@yV?3@M zZzP6uOwsl?C%^5mpT?7Wold8-T;#AB!9QN(f{Jk95gEK%FyuxLkCV|-J8+>;$wx&n`ZN6pa)!DHc+! zq`(We{|~$WKNbBizNh%!;$`(<_saUP+lc-buc-&Sp9f^D%eKagUo2i<{8I7D0=qZl zjs7b|)!3~RWMg+d(SN0|7IxQ({wu{b_PNqLgWazdzqa?V`z>Ji+rVzcNlG&VyWeH( zevcGa2D?9G?A~1bQSrycpA>&uyoD4uDLzsHq%j@=h9BsplyI~8BIk9V?Mn2&_?O}x0CH6HlG2Ej{Ycq=???ZgCR4;I z8JszQlmi98)*bKeM*p2=MsVB{3mTd-k8m_C4mH5JM%l+8*LNe;KSlqYh0Y?=3MUGC zNjZd+Qc@0Wi0Hr5n~UepfV0WY@Z8zdd64s9XQ}fL=b@w=MoJ4(T9R@&DXmCp{Xaba z_joR_z&Qxff9GIQj@V1lf9Hr@jQ%UZoan#vMCVBc>=`4Qgm``-;sAGNqW?Gyvfk*w zb6Uek|D8wyah{fm{yWd$=)aR{NB^B?L)+v;|J6yP#5ww}PKNmrOGp3JiPpimWogEg zo|8wNv`0Gt&hwq~vv&ZTyaBUoI{-?O)Dts107_z~I{?lbSX6E#CB>q$2lw4LZ!s-$ zF2oo=6Gy>rb1tqKuk%jlU3j|PdAIW(@vd=*1|PkI!!$@~tFI&FX#E)*&N)@uV7#TP zPy1=o($>py)6IkR)!eKv$yzgN1FNxVxvRk=*Wl$TV`H^I_*g zQjX*%LDNx^+qA*7QSwM$(<_qC^nv6zt(QuqfV7|KUFTz_k8xAj`_9Kr@0nhd8kydd z_Lmx)UY8D#4wRZmO-(-QA4ac&U*ZZoyS z&06Jnvd#Gh-dp0_gxAI)Rk&E)w95IuX`8VaotsTtogbN6I6uzVJI+t=`A?l&oS(6^ zgWa|9(>hL`K7I1UKI6xZo8D>i)JauS`%IoOX>=8GvP#BJ>o<8yzIXiMot{-=YJX+u zD5HVO(c@>}u(|y*jo0}ghdjF(H#3!%cQbvybjIM(q;yIfSvWLHIm!xqy)&lUhP%Qr z=k0KL&9t|~S1_YEREd-xc$1X&q;xDTA87ibFg|esDP2m-r#BTJD_t{flellKwEX1q zW#?~wEi|}K)u`!}lg3WS+-c+7=KR8d9)v;i-)ZCIB$5`+?M_^f%y-(5(xK*fTczAO zta>eXtE!xub*QcLC+E*uhuTKNv1l?GO-4gFoEEp%#Bp0iJScv_jgP4W4zZ2!r8doR z4{t0KN+u%sEIt}ZM#ABEBpBm6ZK7e^ZWBobLkV%HEgMOEGH5UniiYFKWF(ph#(n5vSMoKqQy6P?&{-x{6%Ie0yE?8@S{bA2Z~#`qRTIDk>_ckLxzIa>}^zH~`aI-O`K+l~dD? z|HH_WX^G42^04^e|C|yf-11e=?N6?Qc}=?x!MD=K2$@z%6=FqiGi`OXGPQ8E&R)%~ zBV0jO$Q9=G?22GvwHs$FEK-imI4!y-%V#0w7_6lXXjY$3hBej`>qxq1!}QN1VjZPH zGy|~bkkSiFsYA!(ufAt2spI=~nlK)>?KKl37>a~&X9}*-NW_x3Wr2mN)*}euVTJ(K zdYqQOTF7{SI>y!0b*!tGtGBDdb)2h@tFNn{tG{c2YoKe8Yp`pG>v-2t z*D%)!uHmi`u1eQP*C^L$SCwmwYpiRWYrN}3*GZ%tM@nB(29N@)X(%bfNvR}d6e(j! z8BfXtQYMo!m6REz%qC?HDRW8DNjZxYFz$R(E+pj=QZ6UuDpIZ{USxL$xq&!x~`+}R))^#%b80-OvlZQj$1Wud{$5V-T6z5+j5`3{v z7zwHXl~5`fP9;N008H&GJX|x_;d0GlbH5)q)!*1g;p={45!`|l2Bah-q4a$qI6gHP zDT@Z^;Jd0~As!%_!im<2 za4epT;xun$dqnuY*yeMRaomj+!Hr+hU^J8n8&?M5ww44g+zp3+lO3dfwG9} zY^H1oQ+B)&=z>5dqd{C076Y;dlj3k{93LD>#gl-~6iyJw-84c<6Y;WGgs<0%MdGP& zFl?lr1aWr_?kWn#aidr~5v_A2km~|zlIudl`x{2e3Br5AbzfoJm>G=6B7CSh-xkbS z!npJ#UWR5;$ryNyc}ic69t?vC;BhDt4<}NgaGraAOeZ@yR$&Qsy(=6a3kn;{aW^%VS-O4StU$Wt>oOxa0+=#^X5nPy#1tgBN^*4MYo< zz$N)YIlxmW6pV^~aYjG{EQ>=VB2c$cmUUdlhGXPov1BR~i>8v8{+uarErm?q*sj)}|%!6wVe#Vl?1m6J_j|XE$$2lL3@|{f~93YIz z1F7*?926w$61+mY!u6m*@I+FO9iF~p1~UzQVrqgx*c4zkdNJM#fwvHiFsKBj3D8)Z zi>_6eyz5+#F*TeLt{^2`T-6i7g-9`oCss;030X|=$kS~?AR@_N3Kk5IB3h?S@Mo=d ztLrIapwmd1E@lS&i6pUfg0W~U77ay`g6MD==muY~nv>B~G*Zo=TAxipp|plTL}Ec8 z1Qt{>7zyQGP3?Lfv@CI?n6;rUlfli~v(2)OlEEEF|ggD>`IlkW@iU$x*jEUtR(f~mSLI!b#K?t|;<;WpIK^?V~ zE);qhP;i=9z_{WFP?Lle7zDByZgV&S?GP914Hqg#Q{kKosa)@&rN><#FyBsR8pIjM z7=JR1aR#A5!0cp5+?*ur%@}Tfi{VNzNHxSX%yYwign-eBD1J}jnzy>>_^I}^Ym31o zBIQhDZW2HQNLU1HO+iH2a7dIv3UF})m=0b<(W+n)Fc~IrGz$5FWHKP3+X&y=g*AtZ zs-QTSw>jed1xS0*^%avgk4ZaQs2?n}6bm52W(iD96ioplpm{JekZMLNEEJ)BunEmm zA7OeU$qbs{?FdjUi4Wo)y4rn0{J+;;bNyhDdM+vFWfW@|Rz1%SR!tJ{WN3ocP^?gg zfF&RWbOF?yfWNY22r3wA0Qx4#D+Cv{=G<20+5zg`cKycG&1dQ^z?H~zBEWFmWd?-< zy$=r~t#Qy#7-|h&L&?J5h)0<0)=+@|*pJlFZPGTo(}B&4NI~K>#~t%%aaQ6HfFH&j z!L6K$^uod=oM3MhR0B|;JOzp3vq>Npv}z=XaplO28{tmvbGHb0UdrSln;OYvP~%as z1uHoQl!r(Nm`%bj0-rEXtg86NF<2}yD1!*vhTaDXN5aC)h_VvGdX9&on-cL{d0;-*o5wbaF>|Xjl0B1xsEx0JvU`&@)*`<6gR7|A;3)m%Ai~rvY}a^xs6o{ zN^uEe3Y@@2n#nL^8lQ~@`C7sRAQCSH1JO^!QUq5rWG zKmhA;NL3614Bt3}u^985ibAks@UJnakr=u{rwM>6-~($Qf}y~o&6TbrKx-p+h-tlr zX}y(cg_6S6vpikEJ>VFl9tYUNSd|PufH&AQj16FMD#^5_Ak%=zAnu{TXQ8C9&cJdQ zJg`4t%;lnkJ7GT9oiu2@jg&>aX5nl@;f4XUfNxAZ;G1CTb2MFvA|)PrGR%Z8qil9j$!S#+@sJ|m75QbQdXcbZ1NZf z=Q3C^tYNC*Y(YM%{ge=!T^Jj%90US34etW|0B<4l(AyCZUArIjGQmC3ZIpK^4>_@) z^spE-994L3Z2bcjlNciy4}~9v`3D;nY{dFb-wMg-4MhRp74r%4FqUEp3I=8@Y%{EN z_>MWua-$m5J;Obd3BEs%%KrA^9}-TzA^jecTBdd4g?eEI6Qpu)f&~fcDRLfV=9p zi-?E&Opa)T+-JGxxzBc=<387Yp8I_FeD?+J3*8sFFLqzzzSMo0`*Qac?kn9_xfi&v zc3em5cyYe-p3$}^-qPs$6VtS4myDX)?81}Sfm@(wBQk@6uaACt0$l&z$ELCRO8 zd`rsrr2It64pM$2LWEkY9mq`liGyTgGoJ<)Wb+^ zNop%nk03QnYK&CuPqZQRNK)J9i-ELpKf()g^Ae_Z_3v+}knq+eoo*_g9 znMQy}+&0cCD#W@`*i^8Jch)UBeb)V)`*{OMo^@g`6Gbiv&O+YVUrxdpiUO>HLZO6^ zp94JtQDpxEy2bF2ppGC|h!ZE+(1N4O^diht2E0Vj2F?Y-G>G5k_{`{ggZmYOb587_ zt`n=0@BM}UkwU}+J;j9E4;G+fSWbwRgxELEVIJEr?l;|=(rvXO340B@oQ zhezNEz^gN2Hn0lHU`fJpi^bWrGnj{t0giARK|%_!n9C3Mr|vE8&y1OP#fc#HtK2X8 zfESKim!TeDDnd=L3d1ft1OS!>!iunq-t1W{o>WSOZot zR4Ge90_g^bGa#&nfEF8PbtwMP{geA=gW@-x*jL-ca?IEUD+ZQJ9BwNpWKKhQ;tFaA z6dD6DP)r8F5P80D4`DM%8BA#IHwKl54uPfsYKUOY3=3)b)BRUkT0*2CG1;)3fu(TE zp-rN^vmx5zi~QL4!GADc>pI+0M~~{!(rtxFc~`Urp2KW|3Pl(Mt0E-E6%mdpKpZv! zq&fFScaIHiIXneC5$|InBBXp^zzJ3atUX?Kd^}?pSa2|Q#3fnNu&Mk}{ED$(I@f-y3><4=H_cUgDasIC} zM#`s5FC?Dhu^}vTOg9V%4t>C8WAVpwgFg>toiP>jb&KFIB3JCILcJr{R>yeq9PBCe z9Ae1bXU;e&pQrhZxq&k$;0->N@anOU;h1p*67eN~k^!~MX9QCiqp(6ktdvubI(RYc zG_#2ZhUJPo$UWTCDlIArQnpo#3Pza#lX#AVX#w#;qmW@(7g%YCJmhIB?1`k?0t3IS z7Gbt$&={h^h%sSYa6a&B1P#IDhy;P5wcGL}Fs_uR4Nt?@n1&Q7-x$jfPBJDPp$v|P zVC?~Sg6#0ZV&<4EupU@q^vfUxPy;xI0fgeN_ykI=02SCUOeUeffZnzHMX%+a4xWz2 zG;DXaAqAESt=DMq5Mvn?q-m4Uq=MKXHBOn4xJO(zy`(kQw|2YSfVsL=q!UDMJs428H!c&=U>nKuw7i~dp!(V_?ZO|ap zHV(nUt^yvjf5>x|BLSW&v^CZcGM@pTS%!#%}E5kTH)$LRKS z&lw)wpx5lg=A?z`g^eft7v7ry(!(?nY=$F)r3Xh2rV8u`0eEmY;D9qEy9a9ksxf!(i}kGMT+exFQRzggjmZUcLT;ItytBvAcWw%zg1)fjf~T-;vAlDp z3~gQPxg_0IXHp&976P#dkRxagw8H=ZH_SO~r9@!50H`<$3&E@1mgfqzb(IGfGLl+| zdFVoFk(dXF7%Uj9a?C0l9uX`EOanY;EO120u@k}@e4=B>1?)$t3dkuNfIJU;bqOSb zSqc2*?Zes~quU!yT|CAu1FDl$7dHiDV8ExiaFgM6zKCNx_`)1_d@9U_`*Q;*Z9$Zvnd~&hZI=SKTS|EcM*&xyO(?zq323 zCEOPJKs*8)jYvBI+CmgSg<;-7=|hlU`lZb~%%q4>V`@{FPs1c*y@HuwLm1121vi(= zpm&8w>`j5(9;EIk*vvTqP$A(kSjBM5yyL(eB$_oNY)#lw`Sk3g9&Aamd*?iw)cv_F zNGswi(4|E8xvHr2-l2|>!AvO*eHMnQkcRVXFP_V%Z<1D9V^t;aUg6BnJ zCYm~Xl6nxUFt+cYVlc66Qz5~J%`7Y@s5p*NLz^S~C&Zl*29^%5e?-yX`Ux3@ods+R zLJ`3hh0e&e?_NR2uX$c)f=fa0v7{bi2t1p2m2vQk!$f2v%a-kK-;OOd1Bv;W4Zpwqy_v zgk6Iz3800LcrY3PC5AfQcZ6|)L9!){?Kcr;MocC*;3ELK#I1oac&~cDBCK5Itd$X{&VoZC)h^Dh) zfE_9XG$0!QH=!MctHm1&SPSq9VI0-&8U6m^+2Jwnu2O@}KBR`2yKLfNrxcwayUL>t(FH6{X@2M!Jf3B3%zLJ*utVH2H$&2WG)W4ZhB9$abVHF>c~L}~;C z_a!yT-2$``Jcpx*1&W1&euOIxkA_2yFkX3`8G;wZZXg5XZHv=nB>Sv7z+XG7n)D3HDD6Q`OqoQ z5uRdHNL~_KKG^bOaE5(>jltZWL9x^8N{dT>Qd2B0yio~5ktyYvkO=r8GQ&<2tPHGN zAWUAp?Je;J(rpbOwJo;=y$$~cNCmdTI)IhP!h*;QD1+O?B93{=8JD*)#&w{#3C~13 z%)~%aj}meWH#H8CMSO{4xzKZl{SPi8I+XF5Yn^mxS-~cC$j#Jo5$Cf8- zM8HM34sd%9^)~Z1H)f*TIf&E_+%IqjTQD$G!4VFSW2ev1@i0`ycsLRsG?X58YIY@z zCqkm$!%cL%x0ScGx6FHlH%Kb1ovx%pVE}ykQ>W z*#iDCU*K<4M`w{Y0J;G~uz%t13v-j#9z=)D9*(_3f@+I|x2^X`Z#(Z%+zWJXH|O!B z_DFB=w1#I6*X_p>d9J4kh*2YC;iH!FRCm0e=b1LHf8tcJrYGpkS~_Vbwq^ zj1(nki@GdGaEbw&DIfxr09|ufaICkNx3{-~hX4fa?HoqxacLF^N+C^%iNRw67eT1l z{L050AQl`iP8LECE+Vp<*hc>c4nXt=dIxz28$#IEc><~Zn9n@X|ANXq6K%*GE=9fF zdxCekcZ9dnJCana)WM{J#!6DFa%FA|Ml;Sko>?&v3vmRgNPjDVPK?9;1g0QA0|qZ# z%bju@0Y)qf;1>LU2D4m)$veqA**nF1GIs-c8{))JhNfi;J+U@{3ni>t1o^=7bQ%xd zf)v5l0l1>spyAzNyN5R~p@d-mAt)CTD_@8mv=$QCfTX~VoJpVMo$WovdnylMIH@N% z!5QSH@vb?Oiu4ZNQe~CT+f;_!@jEaid21Fp0MJjr11mY7++hs@lHe7<$>R)=-2E4? z?j`S;-m`cJqeva;#9yO#N^7e*t)X@ioMGmGBu-91>=@#omunaodpR&dSwFxUM0l-Q z9_wcfL}0%60gHh?DBN*_0p z^0{1NwaCgx7C--F#=6YAJbSDUpsdHJIe9MY{tsZ@+*VEJz#@X-90|^yYjEM=hOYtg zt}&H+pUl(%d7tJQAn&uaYk<5jfcBgkATt!fRIULs_W&PGPS*gLyIF@jUDcHXeXAy* zFrjMn9#z+PU-oXut_JdMtf{WS%zRVN9C3AxI-Sg&Gu1%qw4GK1c|T$)`k2%iEJb@z z4dmToT9h6yOU*X*{+jW6zwv&{${XQ*IoC}|+22yA0>v*`R4$)u#m$g8K#QcsL zAnzaEKS`ZM+KqciiO*zO4@=-DybxrE&*>z3-T+Sp&@cBT3 z&+n5-J%wAqWc=r4Ouqenjq{c}VDfy2hzUd)ldm}#f#RjyGA7>`cD=D$r$ABrPWDapP4i7B^(<1qCv664 z=cP+le6vK!if^{>6yK@7Ilj|;bA6}#&hVk~&3C5nEZ;oe*}ijp=lag`o$s6PyTEs$ z?;_vDzDsRM8tA@zAuUm$fo zsT)Xrjnp?teT&q0N&SG-k4XKL)XzxWM(S6jew$+jbA-m!)`#0!MY@KH6kM26yHm2_ zTf!wPzPs4OJC}X9>de}`Te9L?ZYuZPXLu0vNxeWsPx2(Y?!A%~AD@5~@jc9xUBr}K zY&gyU7kHqEtT#}}ith<&l5e#^>7}GzCJfVtC|U76o06wzD@@&WKF*v`Z{v0sr9EA= zx%&kxz8|$)eLookT}gK4>wX+icZN6Z_%V8t(M%lwK#?%kw{ z{8>o&;-b3bSG zs82C@PxIt8e8Gyp++FPNU`*e$q#|6>P~|Fqk?YT-tz(X2JCC(5B9s5RT*cojg99%b zE$v3RioXw+tN8mdzhB}(z06G+TD(EaRs2KDF8}cc!(Sow)qPN|;vd1~D*lm7D>6n& z-N>}=gK`!BIP<}Ngs{2KH%Udd=-w_@@lQfBYl_{5OHP z8U9KjB~PU*mt$zt;bh|7rg-{%8Hq`JeZ% z^S|JK(ZAmRlK*A@2LCJmSN*T~U-xhHzu|w=zsdiW|84&}{&)RYw?B~j6RA5${hd@q zZ~rDuCQT#FN}7YTLeh#!bCc#HEkN4-q#Zz76VeVQ?NHJVBkgd~T9bAJX<^c0q$Npf zOIkb9+LP9iv@WD|C+!&0jwP)(X?;lRPud{TjwfvxX~RhyNm>Fs}DOX^LMICNQmkh_#5+tKBS2@x!Y!`2Uw^X?My}N=%r5_e*3;pbDBaKJ8V@WT`fA5lYEXp3;)`2Ce&%)+o*4 zhA%-W>6#I>#?=C{dj%*ZJu@vGSS_^ww)~`|0_;zfpeTYgK1fzO$XIj@U3^k9$kU<3 zNX^koNjtXp+q~rup+oNOzQ_C7zYcBfb5OjkUo|KI799J^d5VjViwM+}!J}5gW z5rx-GYAYclyI*utGCd=0Wuhf6^Z2(VCndAN|FI>U9Ha$#P$A|76j}opoRsLE$t7g) zKSElRdu`xylag~l>Z}r!Uyv4OQjv-(5+ED8*reoQ&lx46QiHSFKRM8q4u)T9I@ z9fl4)l1bfzLX(mkGA$j&E$vpBNeM33@my81kojNE{O=(6-|$5yB}+_QO71jdtrKaT zxm#Fl4O(PUaxaKoRI;3j?aIV<<6axS#H3`U=kAh+3|f1Tc653*?}Gx9k|#1E(o-;a z_sdI4p3bz?i(A^Q;*t`aH|AMe@&fa}g86@(q0bt=w4~$}&kH558Z*$Bw0`@bu%zTo z5c^umTTJW#CU&49;tgI_Qu2Z4ostg?S_hLhguC50MI|NTTmYtZXj(>gx1^-xtBkar zAh@j}iHRM< z#E#`o8@`kzVD?%A7K7CBq@9>q(EFm0Bv6zQk_jv%dr(Fa$Xt}EP2!eztB52J;3ASh zBc6dNJOd{S$!+iwl0Z{$v%o>d3``?!I``U81tft(k<1@x7HA$gEYKp*lC+tmokH4N z(ulOPbJCasWeDqa4Fs7XvzQ^XjhOV_Eg%WR0&#D4@f1i|&1bTS~1#qqyPwzRT zoh#A`_Dzd--MfWW}OAkxk!?LyKnCGASmuE~|PpWB(k>$H67B_g^?A!5_I}Yw;H1C=?fOb|c5;!k# zzLDDeA7&m0E(%;6xFmn(abN*b$dNz}D9%YBH!k)L+#(XlgShnqw_q4&=JO300o;lb zxKm^vZ$AM*>KJI-W>T0p2Pw5(O#js7k?dV6^EtwQ#(r(H+ zJzsBx!}CvkYxMBS9ozVxiuP8$g72rm4${q~<;`Aha?CG>>@IdL`#jwa-q-@4CG#f4K_>OP0) z)_s_LoPOY8Q=7me$V}E2lJ|Tsfu{mb z2c8K$8+b18d|+MRg}{q}^?{cHF9$XRUJ1M!crEaHU}NBoz?*?hfwuy02i^(18+b49 ze&B<^hk?z3k4RfY+8w0bN!n7Xeko-Ez{E7 z|Bt=*4wIy+8vm;orn;(HVVPNhndzRMo}QlR1%b_hAh0UY0HO#6f=ZIClH^7AND@&D zfFh!ZqOJi^l1Pvsf@H}#iUbt|l&He*bMCEM_fE~M;`{!d=lhSf&$3HzPn~n`J?Gq$ zk5S?m(&CLTYr#Er$rQCIPUM!vPYp_o3G&lpjRIDh zI*_noT7uBUi~4xVlxbH&`-HhuE$*ED*u9Bnnl__|N=2%LDXORH*)#-Ouuxmd(P@Kf zWJ-^TKybU2cG@Z|XU) zzt_F#9L0&kYqL5Mv00tgKl3J5Op{Ui3ebc|yPTGrk?WhL?be?cbW6{9y5Up4iMm6& z*V6HX-sX#`iJzhi%PME|7ojQ=0@kLxu)2so@Miaqc+Z*r>$^8yZr(H%|7!jlsc$rZ(7xxavs8(1$mzWPCGJgEm^WFW z0`+K{J?K)&iK24iy9L)REVs`xZ@ReOrQh{jsc)iru9i`2b5`ntwUqd^zUhj7mwwmt zTO~}5d?He`Ej{f?=$~!2=`}3w+J3U{?1o>XC@rMPKPXWeOQw!CEYqe7uW7???th;pw27MDF5ELiKS!>^E`Cy1Z?Q1&tAhWdZ z1!Wdf5axsZySg|1PPd76^Pt4SXde!5(0@Z$hdicspZ*N=M1z{9qRYqn_i`_~*`P#M zXGKZdcu;?h>r`L#8u+(&<^GSkH{Dt&t56i(&}o=L#Fkc0G?1AjAA4TtKiK`??d}I( zixBI1wf|G@2k%@i?yp6##!3GX?oD^=o2J~(*COoWRr){Y-gK|QiSC2CpW5Akv*3i- zPuhL*?)BXA;eu#D-6xq{_0UeC2WZuMd18r@l4d%FZH3e<*2FD})MJU8% z`Ax|RujQWZ-LSt1g*?1G?tCq(L+_^jL_#blJhJ2TwGP#5nhk;xxXZNJn&ETP?}@Hv z+NhcqceCa)X<#aDSlGYJz3owbTdo~V6lAna;i8`OHlt|vlO*=N{-5-p(f`x_Gy8ug zJ&#MzpQYz%>G_-VyjW7=`p@YJ*4NERCv?DirX8Bi&S6I{UBa|4r{M{Z}fI&q~iT((_jh7)%8hR3?D6-Em3?qGtlN^i|Ym zc5$Ze;$#fzu1M~wY1(ai36`5j^gxlJbGXaUn3 za*FSka9o`Cn9;%>8u`)12m^o~O1heVhvBXLxAh*-f4gG%vh=(pJ%4x97#`hJ2pXsw zvmlj^tGv?xaR2Z7AL;)?|D*knO{?@j(f`N(KlT5)|H=NRrbYcv_dnDBZ2w>T z|5kam|M~tG`d{pSssH6^XY~K0|DXM@^uIa~P5be{w1E`{RvhRVh?j0EJ+Da5s}e^N zS0tV$@d`_~ka$IjdnAq}u1cIpoG#s5;!NV2#IKY1^%AeNbTf%pmUz0vt4Q1{ao^JY zB%UGh8zf#;;?*Qxed)du_e(q=@u0+Yi5s9kkPM^)*+6aJbpx*-SZQG8f$0OQ4D=54 z4a^vL!@#Nos|~C^&_6IRFgQ>jXbcPuGzW$US_2~k?SavO&cN8f_&|4H=D->QYYx0| zU@eJh)#_5h`t(a8GS7}Df)VJa`cVpl<1q$x1v*{Z%3y^-yxc^Ui4jl zpPsknSvGiNw9(*Et_#-SG1>)d@K|o{eKq*~NKPC4fyD1zy5#?c?~jVsSF;As^sTQ3&j$ZxgXbzG4gO;AeCX-|Hq*Ag z()L#7AaMd;j|TB<)KVojG^-R~PM`wl`Yz@r(9eVbMwL zjTStx&%N0xJ$#h^RGR27%MzDIANua#uLrLf{Eft0OZ7LBZlKJuUZpp>F}k(VSD6w0 zuJVTH{>rM+b(PgBt5^D?d!mP4rP8PjRhpIIXpc&(G7{Zc zX-9WQ4^~E_2Pz$=e~(qhE8Xb&%FN0dl{KR~DsQZ;Rav{TPIPm0S9DutR%Lc&PGxRo z9>;g};5CESvhe?CZnlj94&Iw@*b_bmIQaYM@ty(Rc7f|YRN0noesu7$!N*rTTVsHO zPY?riJJ`PD=G#wv_ANVM=Dgd*04KB|G9kbPyFYu+mAkKKM$wxHVEnG74=wGsHyUdm z|M;)ozH$4~{2LO#N2*sWcfNe(Ob}jg@TtMS*!l8HXZ!K;@ixf^9D{7K$6kAFN6-&* zb$%Gq8+>l?`N0BZYl-%qN)wnzFBX5rw=gMXL!y;5B%59$rRqCvfdgRj;Z>nqh)u1~M8Qtz$z)o0Y-P+zsaT7C6;e|?}nSg+R` z^`UyRK3s3rN9yhRXuVS(tB=>a^_lfG>TA~DSYNBYc72`ttorQwoci4QygD}1b`rl| z;vFUau*ADcyobblNsPt!F^Lb5_z;N?lX$7bhf92v#K%bd1&O~T@$nLWP2!U!{-(sI zNqoA*Fy4i7%G;Qi*>p@s$!^E%Egd-z4#E65lEDJrX}4@$V&m zOyWOD{1=IzmH0V{Uy}GA62BtVid1`~nn<-)@;=oUYY4Hv9`=5`Jt4%Hyu&p$gou}y zff#lcpAM~Y9wqwm7auZ@&$vQx7UKn?xh8d;W`c@!o9H}3`X4dLNry?4x;NK1s&5?4 ztP?Vj_=6IEKw?q`tw)LQlLw(uKd_-4C1pD9p*M1lrp}?!DT3~?jy`P|tV4qHCvU5d z?^qkcIyB9ANA;%A=avB%Ihv)oO?|Wa=Fvv=Efh_z(w!tW1Eh6ukIuYv2;tPg$)RHg zh?EtNIys15&(XO_IM)hv_%1WB@WJ7mD?YSCXVh_tYtkW^#}#N&OnY>~rUuC==1SJL zj^@@WbI}w0h{U_>IDLrJMPiA#$xY*Rot>dBef!vc?*ty0@yK4~=Vshp^pB;@koM!v zH>BibtG|~J%|3OCTO{61!R?-(1Y=E!^Ee3F{hAQ64q4IwfPL65LogI4;2mO(4zPOn z08D_10ICB#$+=(OF*>JiLLL%-RN_4?y=a7tSr+aw-Bq=ZEUsC95ZZUUi@Q<@i$1Xq zu8Fz=7;iTLxE%0Ar(&HsB9%+(l($H{x2~5|L(Y`I9V3$2DfcYIyQZUcbzFh_EMG3M zbpoQav@O8q%cbA7&c0Fpph5pcxRoXKy?YL8Qscq^kBrrn2 zKVVI|rK|6nmbwL41|~8IV3)PbFhXLz%HKN5(ZVkU_v;K8oVHtCbB5xN>$=HMTvHvs z#sTYgn^2lLXFRk7V%>wg)=)m*X8a-Rb_-vu2MuJJ`WF;NU*L%=uYaR>Y+WM%~J#7%{qj_oueR1_jJKTK6!W)T~({>wu>Gu>@%efD8opv1YY6JQ-qEJo$86 z26yT7;({Q};V|)5M;sKqGg-i$*>iE7svg~*<0K|4)3AbWkOxc-M1f~OD|3spA-rcM zu^H9FMr;;o>i{wpmPe20ayD1gsqB&X%L@D}mc)oN>2Njo=$JW9h8PBsGWQS|%Z~M<#meWr~*~N_vE*Ruzw#uA@XmN)QPt1raH!Yj-bAFv_pINg^1=`AyHwbs}lH zJts;0^@4F={(wVt!U4ph4gEEAm_PIEmvfa9q0mp+ar!#;-h==B`Us2aWE@?$&NVKUjZS7xNK=Hs)k~@zIPJ{Qr5x~*z^LC` zzoqBF`mGB5REfVO@wXK^Kvej6Dj>wFa|4x46ne2CM2dCkR>r_^fsBEu%~i>00zNI! zg+`Q~6kBEucvd%Zb)nu1#8MqSl~ia>>7Jg)>i61H`i{ik%{fSgw+F#&6MIMs*)vu? zY)TpGr*Tpd~HF$uP>Ocyz9U;X*P3 z3fN4)0Cui2k^&g8&H^jUb$T6&r+df^%8`Xb__#pU(BgcuC0RrvQzuLFSL0P0CS6kE zvm`z{r$y3*NUw8`nLDDWG9N+5t>jNWaFHH~NMJ;X(-+M)F{euh4;GhvX`327+Ayp^ zF_XmS>gkYyXf0S(3OyT`K_EKv!Or9`80kzQJxdE1g~|cWm#lNYLCF$DS*@uoX}mrj zX{=<)@qCGY>BtdzfRa)?`9-TqaBd&Sim*m0`G@EnCcD;NV21q|(4|76VEob)ibfyo zw_)QAy5tLW$>b#}`{~qV3@G>LsATk75*_>jpk}y3HiHL?6`PC$O4W57QDJA6WHT!s z?A^3cw=4dY#FykNHshN8ahd_G2C^qQ>_7*Z;cBFp7LW}CYHL`sRSL)@zAyGvt{LcLbvYa}L3(e5>g zt(tV5KTM6bD@H67z76)#nO`U*B>WXzfKS2R1{mx9Lt_Ey{^*2N?Er0V14v2l109HE z#scOHSZ0GaH8$+syRngCdZWZQNKEpgO^4D!MqWUgY!y}IP*WWCXg@0Mj6MbFkx}R# zMwS{HBB9E^LT5^P8e&*_fvKS(Ts&muk%X0t@{(TB*rc&(?*R?6S|z?k;+rKVr_t`I zN|=I6bOfy3Q%oQ)$u;RZ=Wv9kYHs!+$;NQ_Jeb~LkD6ic9ApR=kq*?kX})b&h-v+UybD z5vE`{YA54)BIw3@0RG;_cDhG*F+^xwVv;9qDUy+o9=JLqIZhP3;xojHJs*XQW?sOy zUOsh?utf!@m2R)b2OB#?>o#^&*!M|%uf*g?Drw-!AS>7HH;IF0)r`*y*iXj_vTS>t zoF(J8-Gg8;q=X(EkP?Y*d|yW->e*AnV7DnZBCzgRi<5+9M0aWI+IvaEc%vSY_(6$D zoXiEG4qGes!^{*!J!RHUK-%b5K(@CdNtW9EHvdC|iofNkca*_B~_2l^xK z0YmkqL~>&kkKtw`Xyn;$CEbtK+^Wbe)CGju6L)TCi**9<;iq;4F;B>3idHx3P`$sNAJ zC^5-ljZgL7-uSd)`lQ5vmiQ^B<1wd=k!$Y37__9S6Q|uFlags%NJdxCj7v`%mQXQk zLhH^Q~U=f=<>hD9NzK=eaij&^S)_=&yQh{!MRSrruJ;fMcQ9 z3@9FHNX~3=zIfgZ!J~1igT>2uJ$5oXF#-L8%PF#r;~OXRKG`@?!M-5z^Af+9vl9mQJWs)v)C-1N2<$FRze_pvdzg1T5y*N z$`n60k#4oDqxYYU)0GQoY}ZB_M;CKkYe{Fx131HK>W~Te0yY-R4($ zRW&lZtib~oUoe1~=7vE^RDAAz!$x}_a zoT~9Fc6_jLsV+R#9nb9D$e>sX{0b0UWo8>uMW{S+52^;6F@7jsOU7R_c+TA+`en{R z^E{lmE1fj!y|S;}xXP~gby9tOt^;uq1wkdP8|9!;_yUtFXJkYLTsFH@nqwv#$OswH=b!c+xTna zZ;j^~&o^FZyx4fD@p9wujej)$*?6V#>QFR9RCZ;l_Db~)Qe9oDgHmlubwsKism_$@ z8>PCAROd={p;XtC>V{H%vs5>c>gG~?yHv?uAlSa0RNpVv9i+OmR6ios-K6?asqQV+ z{iXT|sU9TNPfGPLsftvOkm}J=Jyxp6N%hN8JyEJBN%b32{gzZulj`@R`a`MyM5<>> z^(?8LE7kL*dZAP=DTRwGONQdUIYSuRN(j@Xx{6aL&_O+;TGnh+3bMYC5y|p_bLuv(( z1CIEOv8|#GJ-UEgV)vl|R=oKT#G3sZl}4CCT41Fn_nxFRS; zba0(6jFy7lo-1w>rQA?!XryoJAvLtC4XM_pI^<*-_mEzo!GW@^rboJtror!3fOkSp z=PEGuI=sC9y^tGPqi_46HSK;6OO=j|=88fd6SOHnSkn{lvfefMV{BfEnN`~}qBI9= z#1e;$m(a1idXh^EpT&0XG&DyS-q!6N%>gsB*9^VPK^xm}4t`3nNZ|1^1g3!T{}uaf z6&vbT3g-ew4{p~9*UHesp+$YW4J}r<<5C51SMNp)797ivN3k}yKujpaIH#I-FJ98c|k(x!^oFs`jQdHGu0@$HVIFy5i)NZb> zrH8V1u0-l=bM;Z?kJ`cP0+s9T6aHLeJk2u?1u_v32L3w8%6-ZjD9i;xEG!?(+lSuK z_vs;eq)2tPRA)(bjukjq1PC?1I!g~)XjVK-iGUug=T+_R04X)I)C5w8yGK>yFB;g@ zZonwP{(4pl!X(gg{bG;GTlAixZTpTMdaoinU#jz@x*(TOSpR`!q#(;0BcWpiF{z|$ zjBKqC(g0}w%(c`D{}NCy9wX39gDS$+r7fj+d=_{7auNOD&<=ga4XKS?T`X1pyRI`` zQEumW^+_inE2_qo?$E(qqf*?Mt_0cO<^m@>@SN{2nBWlL+e+-9q|(lveXe$(QR6Pl zCEz5g+hu6iz7vPk$gXZ6)%B(NCP#Mj2toVp&?;*V6BoCaeFW+C^sV0l7g?Z~Aw13Z z@t6c8Vb0!|gNv~Iyy2=64v7NdfjwAKk%smj+9!JJkQ&+5jitJgRF}9tMe49k?!o>r zpfPreeIv?hVn8NULaWyNU7dS(Y{LZi4(o+dOmSL-E^BjtYcoKN^*RYV$s8OyuQ(K8wHlPy(;3CcVPHpPvS-=j1pC?J=2h`|(nKN+I3 zSE`#T=*`?wvJ=!By2sRSWAu;@6=)6%oGXoS?j%qZRFuPAjBV;yTxQI}hmPnwXXr?U zyropPkm}p4V8>{ltL+`~NAsN3SI%r8_Yph7dUO9E{OWmw8h#@{XowMBK*Z-PwR6to zVHNvhEKN*Jl1m(4PKxyo9Xs^-z6*!Gpm@Ghs_&5MR=P#dHU5M^c42`V5}M$8Mu4cX zW1X&ADDIb#n*-WJ*Yhegmt#s#Tx_!fQOlr}SMjWG(ILwo-kslU;y7C(9pAk5W|8ZQMutrvd3-kX9q9e$Aq;J)YCdO`d`@$iSbbE^yN4aHbVM0$K<|0D(n9ueIO{sLkUbqu zUh?dhal!>n&hmJuJ==JghG>j z(e>_{Z#7Ojf-xSfHA*O44-*k+r(+hZe$WDEkn1?NQQ1x!23#d zAF1x=2%FstOcK^?B;;%^eQq!J%v0IyWO=A2;s-Z8sQ!;Yqcq7gCUQr5fu;>x*Qb)j zd_13&?dK_*i04T{=iV99hwif+_%W$|+!7=H52XP2*xW)cd-aAEk7WV^;W~uXAB_=q z+6!F`c6T)8o9;2=5p`kz7^&TwML+pBO7D}-)ONu5Nq##AS%7zDU-Jx=owJz=NGoSPn z2e-UlI@Hb>8+umPd#JA0Bz@||LYTx~;t;dFtTp8);Eeg!9ieSs(3so-6W7xtCh>;W zE#yEJzNv`@(F;Q_4!tx)dn>7aTB@I#zP(hJI`RTMszY|F+D|B(=`jGMfcS_GNX+ZNyH^VRaw%kM4L3H*%SJVR1beG$z0xDN>U-m zq{%|qlxQvma20F-$DxbkBR{5z#L=vBNE=p^ljdSl{kk5{$?keb zYYntx)r}xWA})*l-J3g@Xz8IPFlry$1hwESnOX^IZryxWbDJhT(4_hUseXU@ zE>c~VD_{!Ui!ht>St@jxwNSNtYbEN}y)4lKg_6woJx>{($t0BGqw}#wqhM+M2$5^- z2zR8D)Pm;wn;&R?(2~-Rrtd1%ALk-?GHeTauE&4A8mJ~fPW~&QeJ9+E!31b@Cic~V zE>qKo7(l*m^CP-VXDBuO)Zfi`0v#12K`jGeKhmJ55dTE7Eo|f)DFYt1(ZaM~XZ9jj zi$c6*;)RFM_-J#_rs=;X)t^mAH~+c5jol@qiM-qbjGj1Tz=aWJDdwQD&c=HHd_AgoXo>INQG)uCDcfn~FC3v3BtRIBzYZV(4?`~bQ znFi?AT;A?unxAVP+oYC9suxT3qUn1{^;bJiA1*2l3;%+tYjKr^W}&aWaB=3LsNXn* z3zdA1$@{=g!(hS7ZZ*AVzyuEox^Umg7&AXfTH1BQR z*Sx>^K=Z-oL(PYqzi&R${6q86=3~vrn@=?V*!)xT&&?;BPc{G2e7gBe^V#NKn}2IQ z*L=SDLi5GuOU;*?e{cSy`OoGn%~yw`;mYu|;T48g9PSy8hpWTMa4OYnqzWcCN%a<~ z-Y(ULxCCMaty(H5m>62tt zNd_btl4L}ZjwD@4-YCgBlFX50z9fqzSznS3C0QcLTP4{{lDA3nPD$P+$$KQ(PLdBu zvXdkqmSk5+J}SvRl6*{(110&SB!@||v=p8ne*N%D(R#xxt7N!Ns@F>OdX3&{Vi4w& zPf^jFt68uPNh(-A3+H>!KN*7dpH?MqhQ%GE11bZSubkmkqnX32S>b!5R9*fp(SaaN ziNiv97tv1a%s+XND3ebq2KFjmUMh)yNacS1z{h3K)U9tcJfxsi^3!d!dnlvTNgncQ zgo!aCG`Pmyi)cSL@kPTUylC$5sBYP<`lj3R>qC_-Z%xyR&?+i;LHrW9qS?hYkd$b7 z;o9t%$t(8WFOnroqH=iV@EVnUhKY4c^-ihYA=SI=t%=`o!eJM>K+Q?$n=prq<0+FL5$eE8qaFZ19S|*r+-R=>h{&=olzVk_MgaQjvqv5;VB7*&a%hY7)k&lIr;oq`o z(q<2D+jIOd5pLa|KS}k^dD;m;cvpaH0Ryju+Y62zd}wToEll=*f$Bs`8fFkl&&k6( zD2+d*U}$e@%|KLVPX?HpdZ=nNpu_u@EzYx#afcEA4ZfelhIMi1JJ{uu`0(&YdQKbO zMPWZH)n}wi+fw6bz`GK>i==ZSM)Aqd4(eOx6{eM@699VX!yJ$xFgFXv6Ka0Jp36YiC1*NuUfcRP9b%vLml@I9|nd#?32SK!PKwUq~rlt zBO`^mUS_r7qlZ7+bH(s6z)hw}Qjuf@CyoFTDBecHwxEeo7LSTlwd9tIrw@H{yXSaP z%^#4nFS7e>Qkp-5*h zb;}X#;nP{@gTvp~g|4IvT{-7DvgjwQ)G8-j4`Hy~(jZ|>YEt}4jks69%#lF+d5@H+VW5XqoS$5o66BUp0j4mgV(^_7Ru z(G}0o6~EzM5i@zVBO zHhg){tHZxmH0zQKO410>EKZY!ssg~vfE=c&>dDwl1pJrD?hVDOvcRfcb|)ZUhav{SQ7 zsy-F3!fw(dQgVlLyhfeZBx-?%Rt82qUo9WQxYx1cWT}A@ZGvgYCU$aQ6j9*-VCXjC)eDl;^cqh`6@KkNdn+0 z1ka|!PwURi(vz5N1rH9lP;;{d?6~AYB<8zyl0gu7%S=HDJ!-g{gLRhpgmpgOvvBwY zyUw|i%*z>C0S5~;FT(_&iZQrGOp>dng3@V?n#bZXJG_wGW1P8#YEWX(f3V#CeF~Obz+ejZDcMa57j+*u zv&XU|+grUH$lk3PxLC5Ow7hGB8bXlPCN;)`v_Ynrrl zc?qxctPJ}R-VM}@h4ZWih~U?GWu6TT`%wy@QgZp1aqCUJSG6{@i{3_(JiipBI;i0z zOU{?=Pq=Z&pRbe-GkI$eq=Lr}s{p^iQ= zURo}b0+^wF<}@j=Qlwb32esRyhg{mZHmhT!@x>w+(R5| z@VOBpe9CvdJebTQAL#u<%Lba0ohA8D4j2!bzgHNusJE2*?g%IsCuq`1^aR<#unt&v zNYkCTxC_)vw?vC5bMKQajWQ=6QOen+$g0wDc*Ub8nPCw`xF;R0J<%f^QrNiY%$bnR ztbIodh-F&cqxI3==URIz#N8#?O%l>RZC@-}fz8kd_(AvewZ#| z5H*2rO$7I}Wo~H=vIiGjxg%#wTA%5wwGLNIKOxD-B{{&cBzLb)o68>*4}&Txl|dNB zPA;-6NyNlcD5*N;@QFTnNsSR}4j&hpF|^zP*aQsRGdQ&aGq)t%j%j_audj8iVtTM7 z2T4LssM#cbh~#32D12`aMTZOgS7ZzT+9+J6!=eX`IDmmM<2;@^;!%kdas>at8LV%{ zr_GKAEKtg`;(Gl`-(ZWDn99G0N$*&rN7g`)u5#u!vUR$rli+y-G~A^Zv~%@wEG zN)XOLVVICW+B%8U@pg-rn38--VSic|tnCP1V^ygx7^xm;ysoO0P+@VgYR&CLxTm(h z-M3~-`(Y-ZkwhdR71Y+qPgz%L=5xbUgaN&j$l6tsl02)cSGjC#^GDKW&}a`dRDet+QHZx6Wyu+d8lHi`MzAU$!o2UD&#)b#d!g ztxH;$wk~U3-uiXxiq>yhSGKNd{kC;=>zdZJt?OFXw{B?N*!o@Trq<1^TUxiaZfo7% zx}$Yx>#o+_t$SMcN^+zm$4K%8Nxme>@sfN^l5a?IswCf)vqk zoFsG~HGJ?|n<=QwC)yWAhm`NR`jAh>EmJo2TQ9(VTen`)g=_24gw~_h-4oVNQ}<&d z3OWxjxPkJVQc~ryJiL#_rn_*7(!JDwyUc*Ev|jDoeuM^nlAI{X36gx(iaH!EK^KR` zwrL!Z|1ibX%)GN=l!NuwK0?EYQSRulvZ09Sa`EFeb6n~qI8LnKx+>v05|32-b{a_( z&#y~zk|d;{>M3iVq(D)^@-laaw(M|JiPizNKG23;gDf=l-O$f_f&_3b-Cwznjw zN%Cz;zT>V%Eid%iK%Tv+{TUnpw6}%2NqRkzo=2Lz!!&n8K#5_%n5HpCFqm>NYzX%r zOQmQUXgB@@N`ak`p^;|aK_kP8==UT!U6SuBWK|o{X8g4elbO>GjZ5@bd;F??hTG59 zIaIZz${~b)gjX)d6}!<(HypOg%bFBOs$%i|k`Svb85!^U^hno=kY$qmu%Nvupes_; z^Siy%6wo{lMbI_$+(E<+_`;xlUg;hmSqnjO^av^9l4wuTE6Ey^URzMJVHXH`y3C5p(6u0v`vilYytay zDA5rs-lp#-Bk#5=zDSab-RWo=uKzI&ZL09Z*WyW-B!V0=-^J5cJUL_(LQ+xOOGXOw^@aq zga~y+J50+8JIAv}-{m78wa0U%B+TV97RnTma}Q}j7X;Y=+)$S=mW$mM;zWTsD@(I| zIgx8d_SbE>T2F)->xNS)ItitI9#AJqe_&u~E0%)=fdH8lfCGuyP z2ld@Na?e z_sFMp$v5hf$(_{w)#eZZJ!_A1z5Ot7e7s7%wQ73OFN9hOFBP4yUjY{+SdQb495Hfa z-@_wEDd1Zqxml819pllwqaX*>C#-8mugqcr?2#x%7e{PrLj+_Lwf4BP?{#1pZjU)OHoW9Wo zwD}8y*_X^&ED6J~pFU_XNwGVgvw3<%bJ&x+6!zWu4ryS_ubKFQIUEy`^9UL5T2~=! zxrQSa{zn;svOLr&pHeny#oy?AapV-cVl2!1avgp7k-*~p#*=A6vgj-U1*zaB5=e^# z^t_4SMoy#nDH{2%uJZw1C;5|kptF#jy3cukao!L}N0eEcK?#@R@eY5$LIep4+ub>p zyu2gJMt(RW8PPU=$?qk3SdvGaXqzI+iu_mDf&931j~;u?Am(6yo8|2j0)bH%%%59(joxxv`PI>x!$o;w0d?DQH=9*gWx=G$R!bq&tRot=VGy#`FR97th44 zZb$9PjNRMQ6mU(_Oj1%O9W4hC@-#Wk5DjFTiUwembB9u`?WKBFe^z)nq^bXFCY|p5kkgXFGgCZ>Zb-VE&tqM>z|64 zrWNZ2RN!0Ml9i#pL)xq8maL*@k!M!Yr6r&~Qvn3=(UFFM1qFzrg$jW)zkLNv!1@JoBNM_8h+Z!-;A*E0^oD%B zrRBCq+nwe%?JLp)LO96Db1fTdVnj_S*Jfh9qse%NrxapFN(A{PL!iWpM=j zCD-oBE+Y5c9X@}WJCTq-o*e&i>8@$d)mrKHeBGs%p3ca>EbCZ6!7{3Vhwj$*ugW^w z>$cZx?%7^nVRR%Nm2}KS6KwHFP@$qsYz$ml#0HB*i7`ZpyWQ&!$XdRw6|z$xrS&Z2 zC>8>AvP#*0bMxcvx7bbZN;=btL|iMurq8o`PH5M=pvj@I9!sWZaSiz6w#F`1-JFRR zt7XF69ASKDdrRGpHFeA1Xrvq>oVzOELu2=G(%ypt+`LVe)=QCB<qxq`q_cGU_$gA50xQybCJ+l@*uzR!LX;S!VYNI&X3`dXk{6U` zjzz^FDW!!;W&#h3C@H{URZwYpoKJhZ_V&$V+wW5x=Sn(9Qqmz!>)8d`tJ`m?Sui&e zMJC0|a=^Gd5vBo=>g5cB}q3WZ>}Dt}r>%=ukkONW`k`Nkjpw%anW|d5 z{qgoE{-0NCw-0U~(*ERsT&?|*R%^G9eXZ5n?GxKyZGX*HYqw9*YVB4e_wE0=8=2ib zeeJ*ea1jCFH%(oweK6J93)cF@4qw0a?~|*w*S_YE)0&^Sn%Uj<8ZvUixM*FAjGuuCF|Ga%x`|S2P?Q`4bwSUn*zx~Vh1?>yl7qu^L|EhgS`_lGh z?aSN0ZeP*2DBZSD0Q=&=jZ>ZMG6qolea|mw#yJq8 zNAYq8t~Y@;64qXr4oiwgC&VS%uZ~8QeMT#a=dB%`y|S#4GT>@SQE|(e zL^NZq(^1f0i3HOOr9r5Rf@=0W?~&jtO^z^Cax{&*2W?w#KQ_RUWyxJRS{+TIb4F7| zayv=iE9v&mN&~V#Lvo`^m&CeAQ1Wd3$`B(!&nq=l9$AHs|oEskX(eRC>ftDG@P_cz^}T_oK# zSN20+B%WKkn@Os%3aC+9OB@{J3)|dHzP<$hjOpiiIWV?h2d4 zS#vJrp7?q<4#of`R)LRo5*=#Bv<_8qfX%s(Nr5Ah3hSI8N-8%ip4W5oDCU(y-BZ%N zRO+b7jn&`|9^x}RN_bfN@Vh6k=ukt3Q5{-)V(7uR80h+^4gZVvmU(nt)_dBhvAFh8 z$ou9Stu}W7mdK>h5MjH}ta6>}$h*<0PY$l8B&nmxBncDNwZfq!W=A(3UDC5`^v$}_ zACr`}GIT$+u~I<$wUI2QEU(fRvE;RKlg(-DKg~}rFlAttvH!5pgIFF-GORGsi43M? zb+AKlD*2$~oGkZXw&*!?6eCQx=Kx9RmTK_?a3F)Q$qb$bvj04{G&JK!rGK$UIj>Sy z4yicMWg)Q@hjZSjF~bg4=q@=HB_L?@qUBu528<+DFO+*sTMq;%knxmw@svX*?G4-0 zL7v2Z+x1*Ly1iZVp_1n5uG%7^xJSt7veamrCl`X7$d9>vfs4gO?89+^ZjY9N5?Ce$ zDc|gpG#=f76<;yBlTwAIyQZYOT9-e%hKG!$s|%(>&4kh^cMm|16f)}|I4c(&WHp){ zJz`l+xKT%U8Qrz#+EE=MkbXu|k(7j2_3Eh6SAq;R8ql5^@$j(de}k##CvGFRN1P-X z01i`gtx|%jcC*8j;QRzkkrey%+&sFkJ)9#YJ<77|@)4zPw0lguJTuCRA{lP~5@0Ra zSuQ7xeuBffYxF?fqR;B#9Fxn635_~5ZNN|?F~*=*xk)j|XNaj{=Kw1;LKWH&g-i`g z6_f6)Lr4Fo=fTm#6!7OIJyy~$AfOB)YXa#KnKCTJPm_GQ&w|3f#AkdXvm1} zy@6B0rq9_uq9VzoY7S?SsS+}?6I2zSgzDj=NAx^4dZeQIB}u<1>6eQvGkigXoTAyQ zP8(E8Qt18;AruB_Siy!pUSNwG(de7p$e|m;+{lK>>6U>HaKg4S#EV1{>KE;{mL>49 zqo40xdGrg4>Isq_FX@SeWW#~tAscQ3Qw1GlpP9}iRuZb`srWv*H~HvTt}dV5LPCV2 z`8kwf_}juzfKHZy9t%~nlyEY7{OAciFN|u0KRrp(uSxp#oaiZM2nZmT!EirMfsyf| zSmq=l%u@gdXwILKv>*Wt$A?3dYD{`2xji*vgoFTu3!9n?uri|G7(J!u)zNP%qNhmu z4N1RQh!NhT0?MHITj5u`?Ju}3*%I^N)qUJl*;h(h*N zMt`DP^c@u=-?f?!-YnEs_aOcBY;;Uqm?Ui-h6)zFG_dSIl3WecF~m;}mcVLRZk`nn zjGk>5{5?rYRn3V4HYqlh5WiWmfn0n%JD;ryWTYv{IunS3+#wp36$_pu6wYVGBcm7S zikIn%f0#ptyn+ZQ($cIMO-ISjJx+wRC#-q&lF>_hHypi8 zA^$|uA4_^h&Nrxc#p3Ws*fle=VQ!dRtmI`mT<7&Ugz#fqy&FHbZbbedW(wzknR;C8 zks%6A>>_`nQ25I?|5c;E?cH?rYQ^+tlAbB)&n>2Os0y3|d5t51#IPwD3E5z3^mOEB zkXJz?TcskxIH9#N$GHbEyYX!lN6GGMMj&o|IA{56^Shpfqc>S9I$KhbRP7d_LxvWS z(k~oN`5BDq_Q*9nq1OW`kzTZ@z%j7!@Qk3!fi*1_Dg!6k`Xzl`RN3K zB+X(nHwAHhVl4$^J4<-{P(TyX8tzXoko1?5 zUZ{}u?wSnPNHrjPr-*GyZ^N^w^WK_a!n&T$9ArgMk7X{vnc&F+0wc;a^fURITGr)G zrzC#SjUMgYW%Mz7Iu}bypH<`P2)Zg5GB#(UfW5N>3&u!O9et@IT??`>iC`F$um-{I zsio=YpE;YoN1xIyx>V2RGIurvrwSG!wMDHNHjdKL@Y>3!si0g?ujoPBUeBZr8b))c zQ?e3R^WS<87=6yJ`PY(?qG|*=Yx9aL;c~NPGVudBu9FLlAlH@`U{@^K*l=n=V_O#r zVOfEAnKd6a`VU?6mAYn=a?1JR_z0@N$U8GUh#2?O$iv}MP55j9jpG&^LYam^RAX}f!@Rta zd!NG#ZXDfCHb@6edGT91noiouqIEkpMf3(qub1>jM~~G0czK$KYY`tosuDJv6Kcw_ z1xB{o2&WL$F#bb=@n%EmYKUvh-%rNpiD!)LNpItlZZ<)(bXMu~_FmHIQ#@~$^d?Df z$u|^P7+e#S+!!Ll$q>dLP0hHuN(m~3VOP3Xn;oT^AwV@BcyA(;n68e`tPG@Dk-4*4 zqGH_!r321p0iAiYymRK5(O18 zaAe}?jCZ=dw|BG&MS8EK1W)eEsR;j~UmY^Vh!z@91T>BM1Z0CKjV=PGk;&e$!a7J$ zBoCjjcZrmPPvA;*)j;&nLMpq%DdD+JXIAfho!N@#gOWZV=|lE#@JWPTvRrv!3j(5` zS&eEB<~~D92rH@b>2{<@;8rUhr;+K3-am8}E2@u3 z`g=*~<7u;v&?O2Od5m{b)*pykXYKq6`EE9Ek;~Obgn57E(zt@s z3f0tMX0{1IX%038`#Aa>s&+?lE;28{%jB}C+qAP;?{l5a71uvY`X@=BbVN!F%kOB; zm;{W&+ra3!HVsu>_?}~_QMNW3O(MN^1tr*wdLj_eg!ihs(n{6D{9RoNmLWs7>iuVD zYkOh-Mbf8rSGkmN=LYPAB57m@g2hMK$!VtYkQ3Ey(h-c_y=>>i4+N8&g12V(715fX>GNzNVxOQ{AMzMERADAjm}> zA`UFoe8$4^e3|DBbUtF@&z)U5yLNW#?B3a<^U=>S$ppUz>OPjx=sS=y1#XF7*>j_4fOIjVDX=d+z-I-lzt+xdLw z3!URSU+jFT^X1N0I>&cT=$zR3YUgX6lO%mf(tk=;k!(fDs*=?tTUoL`$ySqWP_iM( zT9S1nnxKn4wdXvl8IzTO7>aFj+N{<$-XSviISZp**7FRRkH6&_5;a&T#7$; zPU(EJZ?N+%J-5G0`m&@X+iFNz`$&6E(S$<6j$B6mE;6226RLjgJ*TyPPJ1l4W794y z+!6>IVp&d_YI4y;;yS(ay}owm`-Q31QoPpx?5?Njj;J-K`#n3C}Oi+ zdGLh>IuQhEIDlX^hMQ{ER&9hs4RPT>AtK|@ED3%1{*1mgJ3lpipG}i&1tZ4skE3vS z#O}%r*kg5ll8##S?mN(kPeHV5r4Va zY)&9_6;eu)csbq;{S>cf6_9{4BFI;0Q zD{wCjxMht3p+?jkWKM9DDwv@RX6A z8IrB)&VOQi6z&=eT+3YiF5yB@1;$$YT4d%PY6h&nIg7dH?n4?)!U%j{HApTHR7i)d zr2v>;t;1qpOB8T-=bpapJNGKG{gSON*??0CCls~e0?#)p=+p_$c>#}x!k`hq2-}Jg z$8Hb_$tzNE-EsT@q3RK!muXd#C)CniiX{1c-%g!JtR$&R)^I8|s$W2PdCM~#^8w>< z;%t!VWRLvzlGfahKt@pu0%T>vdje6iTjx)@LroPW!#RuK_Y6(}3)XB9B0I1Ua0lm* z@e7B+9~C>g=D8S}?r0|2KTmg_>D#yStU_)}HX_+*zG$rL5H?3{j6PQ8RaDXl0BuxJ zy-13qLB`rJ2yXl^>%^2T+8+I|dqG6jnnk&%X09NjNh0sX&P#m4#R{;UB{@vlpd*qMI*O-`o(k|Zu>vxxA=@H}2fKH| zr8yRjRr)?XrX>&Anv$&{na%G-i0HiTVvn%3&4xm}0iMQ9miNvG9E_NC`8;;Ky5Z-= zfMf>m;bQa@Z*F)z8T8}~6|;%BR+fyVeMgVkx`%9S$<}ccPc4G?O<;N|s;Y$sGK5WF zPsb0{)GKXDnu`QOb$Cz+7%i9T#;AelJ8q2h9LZ)Yz0a{!ghMQlI&RzoDtj0gCr$(0 zu7w-e`vG8oI3dd`Iu)Dkuz8z(07;ENS= zlPjlDAA^7@%xJo&O{5FzAfmU9X^Kv^P}jVuAjrU5#q2l3;>L;TA?I};czI!b57>B^ z0IYJ+uelUi9vdC&^qoGYg%H_#lC3M*`i^my=ePBoj3pBW>a&RIVv3 zCW(nV!x06-*lrQ7Ny3CvYYH6BT75qmvvm;Jn1>dM)W2h0pfqI9LXQ+{& zGnbpU$PHD{4}dB>M3{9ym55@D8i>Ag#z@kUY-2r{B?Vk<0E5WRF3ogrwb;s80yKqeNbh5Wf_7=%DQR_{$8k)}qyX~$z+VOi1RAX6X zF9|dgViZ>NBLBi{I{XoQy^8}LKVoo8*l{o@cKB76yFp9F-rRTj7_}0TZ7$hnl5OEA z2w$Lg_vDQZsm2KOpLxcpx0)vZ;?FTJRi}4|KSoRm%iJiHre?+wP=1VyxLHXZ%Dsps za|Qa@vhSL)x7qvqZIY3QYBm!ALbRrMjDmx16=Y8m#A0{I4!{J#_v`f=Bk^&Yp?emI z3}jnOKRmXxuJ}C)dRt40?rn7PTg5QHcJZX*`ncJcqQRezm3*!(i z5#!iCWBc|!J+_}>`a#J)AlVK9rv7dYEP^+0l# z$&n}!&CgQPj=(-Y%aZOncHr1SeJ_q3tcdO`*-nyuC>Qv+$h={N(5CN{(h-5|(D7?L zq1He{g7}5OrdAN~{ZLcRyNZIV_C58RYH%fBrF{UFNAF{w8vFE&Xl$wCx{G8Vk!)9c ziDK6;FRP~D1IQV=%XO=Nk+oAuBUmJt3RFF5?5o&UBaH~_p>O+`Mu|7ajv709Ml$wU z#dQzKc9-mpM%8795Qzr zSh_7L$=M%QmJGc#c82cIAqxAG`N?pvcot{Ur=>*IN&QBc1)EGeYDLK=aTMX;YT|#P z5tXD7D?U5gWb7Qf;{TD%Y_-Z}E;KKVaQ^T%cz{~O(7|q9Dj2==gbZ@L^kB3J0XBHC z*GPkjxA-q1#tp_U)Fppfm%P-m9~r@Z&Dw9=EZSrQd*i?uLo8ZBC2t>#!4bjs%2)6o=be>LWjZ$%Vi z!^^ZBQPd4MemRJ8I#p&wg8LAqFQNvV7?8-#Eo6zHfz zk<_$uy*r#{SLTDU8#$Zp#z@bR>=-?p&)NM!l?dDQo6bupjwr-Ia#5a>z=BoR@E49A zBY?5$aCEo@PjU&3-O(HyBRNN*eqORK! z95Bt{v5_6%vSpyXICdY?R@~dsN{bFWFZlJE0IcOad-011?-kTys!z1J<|6HYixsqIwzV zTKgG^Q@q1|7c2?Sl~UT|0jg_oYFIgD;AOYV^I zY`oU|!1(JF$?r*ax@2UG>Q*XLFFj3#W)8QYSksL%&k;cW2cL6@5Dvm9;Dub$F6MJn z50WcJHvLOYeDw`DEK7Vi-aFp6%DUq-6w4nnm{AybZ}sS`yVr_9cKmdbbL_`vvJbIlyAe?! z(0}MG!!wFrNTk+4A>4rzxpa7@>qy|IE*I{OFCJgFdF=Rlioykw{ZcaWKbOBy`j7vI zdw`tN1`x#_lxLBRZ#=%FdBV6Bi)Fu(>|)6-`PU2m7yF}QDu`Wq^_rk68#curn8Nw- z&6G|yAKzkp%W)h;$*z{{`ZAqtHNN%uyT*-c=hu>5K7C)wNW=sYy2)t+%;GUY zA#+e{({lc8IkZ9zA@^TlND@1lsF{v$KmNYK=f~f#`*f9LS4#HVf>tI$3TS1jf75-! zY7A{zy?>_IrwPjO_|D@WYF<75VMXCu$*z&?Iz1}4R4-smXIyaj7dy0^U4V_KZi#My0rB@_NFDDD&wlqy?Ly;s7v*X6U7-II76?Z#r z@COpVb17N9{f8dPUC!xqg&y|C;X@%pr10EXhvl;LYkYad{g+<5^r{D@k3@$@M?^

D%n`i39fA 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

ue|a&xmZSdl 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µ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 z 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 z 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

=H2mq6);;t`qO2GIcF-`X5yxOt zzcX4}%(PZYlnBNI+-jilqMo-yb~uSu6+%>(QF;lwJ{m`siSzHlzTG3WmZfoQ@5eJI zwtF=AJ?gnt0I-pX8?d?BMwB|JTu*HYHQowD*)KZ*hx(d z@U>HvZTG3fJG6FSm6WHoN07D(G0F`yCRWd^q)xERUIV~}+%0D-F&Uc<=O4(!(ojJ+ z#>es4zv|}PEwk=vw=&3BrO#~w!0iNwDH`s|UIimG-b&j2_UCbVMt3-p&skpqg}-O! z_~bMwziUn^WjI)_?Etj2IPQ)eT}^HEjYg~M?S$QjhR(GQylD%;I#SPRB%PeIN?GPn zwX5m(f4i^!rnWTe)!ky@yAwO4>cU%6F zzy*Rrw~82JH&%^nv5Nl~Zx�f8O5yEkJP(;ZOS&0DqT|jGCM+J;daCLppkBqdHki zBbmkCo?|1dbU+9c%NqdGP)q0tXQ|h(KEr_z#=y>DiXym2Ch3002ov JPDHLkV1hA{m~H?7 literal 0 HcmV?d00001 diff --git a/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..b78e17544c632f3a4dd9651311dbb7e4c21a55f7 GIT binary patch literal 39384 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 GF6w`}FIz7F literal 0 HcmV?d00001 diff --git a/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x 1.png b/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x 1.png new file mode 100644 index 0000000000000000000000000000000000000000..0eecf58bc86bebfecbd2ccddc650c132a00d587b GIT binary patch literal 72501 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%0AbeA=r*7m;IigF=n1-C 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� znse`~P4Ko7$dyijsPLg%^HV6^2}(cq_(IeLH2B2bsnHcd&@B9Sm>Z~Z``q1sL@d8g zS965}n(sU@FxXxXzk;-YTBEBooqg~Ay4{@cHsGsW_iv_v`UyJ-q+Ze<42%Xqv&K5d z2IeM=KZ&_TrLPF}MdZC26?@Oqkp`|yKl06sI`_}p7(%^bQ&$Nyr>oAh=9gLo*&Gw;;XsG;&S%3a1Z7U9=|3eTp@OO{T9zbr-5E4CNy{Cw;SC!qCBzBn}kx7#w z3A6p7C>>a8(l^^HZodlp3b*WBG*LRCf(*w}!4ARa%Z>)#ZNK1?L!U_@3|*89Yes<+ zxZq%OBO(oB&TDz=at9&w0_l&J3ao$Q7itdX5l@I{sJ1ixz1$i95o`l-5tP(ng{q6gzbxQBK(M2VrX8QYQ zYNaQ2w>bQLN8R`>{u+rxGN$5rS#w*HW&sJwX=mbH0OlqV*~_@$O0r@=(}@vf(`?9X z2P{tP{R8<7^=YRI;$u!H%2yMh$-AfkHtQ})gRk|szf*c2q0I|Bwzn=i_05Q8@ z=%ABgU^uOX$F}#creGz(M&GYQdL25*5+2~c^TJh#5}|caxq(VofYS5lcDkLZxgyFu zRID)a>tD~3uz_$fQcDh$d?czL=9n9=Fwi{|kY&7&>UZPj z`1gRpn2L7=7o*cRCQb;MKSX_kB|s3RCwSRm!`#>rKMZp;y&7I8y`SzEKAD` z%ICnji7~&83qz@sIXay;dvWVvjYUD;rSL4b1LfV*A%7eXs(@_Mzuv@XH*M4*O(C%p zT`)n(6OC5`f?1=bI}4P_!t&vWkILm~?~W6bhkucB9&9rY-Dec*RHgblJjoKUS)w2n zZl#ilMbGQt%JDqliIn#1-t@CAP;2RvT=z5}c10V#WWAde2nim3qR*q{6IHeh5p)4uvzAf-K~6 z70DJif(w8gY72VTpbU;P3YQrqUvvF|M%n;+C|Co6_Xu4`QT73S(JZk5(MoLPG?~c~ zgJp6&65$d(AS7O zVvkN~Cp!GX{u3_ChksNU2?e1g^6K)=+p%>*BuX|5?7)`^=t$S3!4p&*jPJS#oJPy;jT2(8y>b$7`=3Z)J&D;QrOic{f_i17Z3NTza%7u(? z^tyL|o?cJ!#ShjG*?43A#7~3{WaR+2_^m_AaV&i9Ln1^EocVQ_v=wvQ3*;%-~E)~4&m#(+GGY9C1jaw35!3lPN6AU#82DSJ{4Bc3aD}`%N&dZZ$ zOj@P$0O|ne4lXD=zb!uQeJ>}z>y)~D2q-|)GEbf(p>?+Gy@FJSRyh5xUu>e`$Ty=xrOO%$1>8SzWB|py_oU>#6>qkccbR}$zLw&LK;fA!PmrvFinl;0o@Xc6+)&fG zUlocgg78q%!Q%gD=rdi2P-@Xn!U*5pTBp>J!@~eW?b|@q7*Qf7B#zC)!@TEOLl_PX z$e}u-4oHKjKTmjyto#QdTZ5{cm@%oP#btT$nyfEvUW(zi@z!?} zZcnuZx6GMT<%w1BUM)#mWY_|01%tMFCY$ zJyu-fS>`kkpDjLL+7saTBc=ih<2Qr)hlI?cpt-`X(P3}3fzR1UAVDM*z&SRM7?3Fe zF3!5RP%b@#=YqB_)h=@(_7T+mqt=__^}ivy&2jv0_Bk@{ACIH19*-^YvAz)vZ6qV^ z3GmCzEa;C>v`t~(%{*V@b!{75DE}B}dLOh>_CcRfmsefmz(R%q5u&ud2ChKnQ?>*6 zwFossy_K5~BB2ii{l%#*j`(Udcp!3EL7-{Z22Nz*h0-@{;Wrp@Tpbwa%b$B06io|I z*~xFkt?XF1Gv&;fM*hU>ZYPM4s9BdeNP4!byqw!l5NCn_UO)h;coW#@9E`}3^X6ZsqG^4zLn*zs#S(3duySUNBS2Wm8e)ik9??Y#aT1oWVB0XCNb_SwB)1q#x zWQlMa6-ppZygBhD10fn+kR!mzI#jBDjfND_rq{4@ygwjTY7r<)4rpx%;o6@2waA8r zAlKW@9HMh~iBpOLZC1$<)Bvh7ECd`B%N&b)Me11>JO8Sgt>crLqVIM;h#h!WHJ~Mw z#@;v|#sVgsuLn|2k;;OB8bA&H6PwrBcy$H~+sIp49N|rzx=+@Q6GAWA62k-cJ%W|^ z7p1qU<6B>ErxMG&D5MuLS14-J08=UIq%YR)Dc@KQ(EW5PX9?zUSUvbl`dFRU$UMT) zDW%(0U%qVTy4%I!*bhR(5Ja7B0ISzX<(vSkCjxHzUYFII-JA>Ca(;d&!m~%Z)kGC8 zAoEBfTxD-<_@chJO{AKQt)Iz*c}tSP;5zIYKlpu(20LYRUWk3zep0nrc7m3><>-g& z&*Qy6WIp}lu)e3!e@EV1{hSXi9Wl0C_W`kva^i#vP!~=_HyLRQPs9fM*n4>+ zT^a#O<<=0QU;m_x#;TiTAG4dl7-ABTfJ)EBrW$| zCQ&C!75*4+2t1yd?y@N0f4v{l(&-Uh#(N^2y!${U1lSh}8{1fwQ-3d7(L!8S2&JSR zO=%e#5(1V~(X%Y|IzTs?RE};vmrGX+r+7{ew)^ZF*sj}~Yu~_NDu{xDN3IZz=PNyo&X&O?ik2rYiWomMxeUO6KN(NkB880xX zN*+N_M@0+;gJYgMYPxZ7vn7quvP0S5hk}M^jHS%o8+)FdN*in|gbBSLUM3v=2oj<} zodN9JGj8K>A}&_snXK*Psmamwb|I(^Qb?<%2_1HC9Dt|^|D=uAjMdpl%YL$pw&0dh z5-D10io_6WBG>?8_UG)n2wrR-qKc@o8wp~Zv1MLZfV^B>fbf4h3nKyN7?07}K=+*9 zCvTA)zrC_OM3+CUK}!x_-Pvuy<$K? z>h|HQx_p_wgC{k8b;42iQ+bH49Ci!H9V3O90&>^-(|)jP|2D!}WQGBoP%d+q3lWID zs!&Cs?TS_E7fIQw9`#q|+Y^lC1R_V=2O@tsrlc3$5QH82f0c<#_p@Z7X_)q8;e=cyvx z*-o9Ko#Q&3JbIYQCnmnfO57vN1SNO)6zEKyCoe&15Y%RZ&j^(syVi1mw(xR9Oa*_9 zhgJOw4e_z)?t_UStWQzgp(G|VoPdZu61QGyFzof_A9H$oIRVN^^xUPyxH|AfcO8g; z413%aY+&G)TI!Yx7CuKQ6GI9XRzpRv^3P!C+>n?Br&L;B6@g!@z9Tex}ra; z%(O0~B}Z&6Kmxb}ELB*G2y##)FjG-sV?a}XGO=1_l|^<-k=&t$f=uEDet4%ap+!|Z z`fF~|+Zr1SPPX7$qPiY{2=a`>-jRsZA94eQSE<>9sdbn%OWQAaOz7>|;&+r?OhdsJ z-V!lqrz`}%E4*kMYO&*Yx{WN#e|PYws_;n_!`@h3e$khR`8c?VIS0a3G24hi`dSNw zZ&8r+w$g02Jn%S;UjLD?=pCFoNlmFt=ap?{5j<-duW~q)#-#a(Ecg zXP}TQAk4rr*+W4aF9P%-FAQEfDRPb$ye2eniAlj*J$&sT`7k00BQl;#*q#mj<;Ygp z-{IbFq$aGoew^>LMgnE7am_X$PId}v2NKI_>t$o>FK_%jv8A<9_;hI=ohbP9P5f}h z@D%#<>3swD+TU$NQ|{UGym1QT!bC?l%nzioU+;5_obcwb<{D~pMmE-^@{{mZT5U!;-#CVs z0vMueLgfOeLG0={x_M%|>x_qo9+zuI=P7Ar`wMA#DXGC*?V+og)s2EyFT9s}0ypZ< zj#*e1R9O}lm{t$5_b^DD=si9nfoCLgx5ysm9`hVnL+lG7>bSU+XD%IK!!o^C2y3Ac z+x!z6=oE8F^5*=}Q`yGsl)ySv)-PYfc^Y*7b0Pzbumh+3Vbt#vFoZOH?3_svfM;ja@FADv&So4y#Rm${cs^wGvWbkty*z_uRH zTau&`sbk_2$Xigmj1e&bPFVp<4;MTKi`aLI=PuQ$hAyb;8=S+hD2|VBtB2{v}|$yt=Fm$8zAP(c@v3q!^@Q?rM&}HofjkxvTUj^cqMe6X30sW z?qen0t79$LQGFor_+^L+YDB8=aM#;|#Np8?SYh}6%y6erz1Bu}Vc3e0Imlq2@GyS( zFCPKL$fRe`V-aL5jKEKlER%-j{`r4v zE=I{kta=0>vfR8}YwIR4m%Hvd{~lq{|7lWuIjs+ueOYdB=5#JD zNI7F`eDQQ>hQ0XwgkSMU#pqJZCQfPM`AyTbKA~?KSECE6MwiY%e&N6JY+q<>Uw_W* zXT^{4bat=9(ygnnPORVumq;EHj2rAz9)}#Q*xnE3Pd!Q*`suUqzLiE?7dgqYpr{&hd^~zxhn*81P}=1ih{hf1_S~JFX0fxMexIy zk?R%s0d>-lyALVqVpsxiq%Cw5o~WomZh+Sa2-N@C`2|q$jt+c5AQ#f0kPF}^6#Rpv z!T$X%9Flh7zprs0?3-9-gg{Ub1?hW_+@Y%zmue{n>KZq(ZzuHgG~m*Y-dz#9@rB@P z-P3}=Yd_i8v!9YFnf|^>)y~ZR+{h@3+RQ^uNG;@@H2ECtr6DpdR|hs+f)3ajv+O!1 z)*<;dSy%H(%)Iw~1yiC?1hD_Bs{&H2BO;@9x4o2`d*# zm7sr-`UC4u^E}{j1b;9S}l?DU1>urv)@FGeHpU~_Vl!ow#x~xvzVFK z6K-CdI=bE~=5EMjMP_{9Mra~)%w`@MXIJj}sDS9|ZsCCGj~gyJYxEhuV-@96W>pJ^ z9o|-xWh2>h+Y@Uq*D6{5q9rO4z6iNa#@)t>d!MOfvie5J>bd7G)9(YAw}t;)S{&Jhjy5u;U71?jX^EX0^7=5PnD2VI_F1TI(dUkGGxHZ46=AkI+1%3FpVE=|gOi9bNmvl#}?3ai% zeTvvp&jHIQ4P$!vX|LWvW~@>%&(oWO_1P2LN`<$SWKOipx-Ig;#B`n~?`2tK&AsuT zsQ+St7zf(Lso3NuR1_lIG|hRwL=c{8^4q+rwS(ku0>}bnQ+Xzm0f{X8b?1Ju6;sjB zSM8k29Sa?yn)@rRAG@SWbmT0#f2zLxF52|Pw7AZDb3EUph+B#zDC94!NKqVKKG#sD z(sYTEmYw#)2UR($!M7DXtM>apeMjy(ye8se`VdaJbb}|WO%5gIrL{uZ6P4zK61$kP z{as?_#pKu9H;cLT7Q=cwNd8elgep`PlKIJqlM?JW!UL1`-q_IaRy*mZ94c2cGh=#} zjGcn@w0`Oo7?ljT&**M%`1d0bz1(;j@r-C{i5btcukl(9o2lQ2!mK$Jv)O{q_lm;D z1A1i<);-trEX#Ct*TJ5Wtd{xdY3Q9}D}qal-h6fJz9v`mJ`%zX5sRe(t{+`V#IvGF zC2YNp-Shj-4!kOkOTEau!(dnC;>ps5bv>t`|Yk z>mRR-414dkd>Hc=p2vfyQB?c&?045M;4(0=1yTQ?!$GZZ_(_f*GY4*&Mwd;aD? zT|%8KzYf58<`zlM&EwrIl{#6;V^W({Uupk8;Y5BLE5zhOOYton%*k10rCNN9?)N<* zSATZzO7+g)9p6~fGt5K(V_iQ9d!QuVj3iJXM|CQgzj1YuuZre(n!6%Lq-Mgx2gHLf z_pb+keb`z`RWev#bjg7D(DbVCZ^<$ZEgtv&iT5819$Xhcw;DE>Ger8UYBtVD+wq*+ z>M~>*YwQNii74rX_HGLw`DfMkQxIbe@M=vYpvXNIt5vBrn|X;kXH@)+S--#XcfiTN zh%XNb{K|rbzbiSuaHl!s^V`nI(cGUkBFg^T``lnN^6>j42wD6^3;zAZxeIP9@)*zg zYR&G~5!?!YfqVlB>{+7m3)6XQuR>TS6{z_!uF@>X->oTja|$?1<9}ex>u&uDr3!O> zqk7G~xa*pIMUkp|xAoy<^zUjP)tXXL+^Hb2F_N?kCiMKZ=hj^E7OgsbTY*=iM={%| zdqb^f_VH@UE*IR|6@4}ZSFx(O-)m+JCqby8_$b|fJe5NSNb1WWI-l8l*3ud+XTHWs zCqfIHsaNk)3?ZrRH;e8`pZ(3>!=iUPaxIU3wDPilnMW{_V9-YFY1p=CQ=3-@ zf;9s#W+o5+^vomfvnb&V2$O)*_%;EnIBqKmF!J%ZyPsskhWwfw)-SJgQ{9y8m4#q_ z6E4odMh`0XpQ>Ve6fW4N&S*8LpBS`3acIGe4^++eJ}!V1yhA9M^v!xVh1*NhpN?(} zeL9hnQha9)K2!u7yvRi`KKW;4Y(c_p?1E$8Gb013YrN;1_)X9QRW~r!kOPjvN`2>~ z*X7je!zU(6tFS(+Lyl*tyR5`P5O5PRC?n&>=O97n>?@ zdO0rMQD|f!e}6R?rw|e~ppf6Xe9~{gyID5%^`bE)HqRh|F3#$v+$2|1Bu>CLq51g) zPc2dTLmu0;w0EgoKZ~s6tQ)~51)`UEKluaP=GXE2t5)`%*qlBrlK2Wd8QPzm#s@2? zQ$UZs_-`~)uV;_g?sEi1;RT*YoET|vZ4`g>`DYSPr?c7bHGU<%SMu~$EMA++DCr-j z2S;mFYt%bVx;1~X_{XF@KoO-ar8jl|A1sNIBA%=l7Zu(4LAuR3;cU&3xGjuZS_8_p zRT6f&@{5&CgTo)UYCgVa^XBJn8W9gf_TuA5a@@xlFh{|0F40%(RC|A&ruJGKFNZ{0 zJ2F=yEt*{P%s0EH{{njlX&k`@c~V1*+DEGSY5Dgk3&ZqZ`@q zHDDzfd(O%aC%JwBN+jNq7RAX5LrC-SKW_JXRrKXWpf-GikPl~*Rrn0Is$RGY zb1f{6oOQ2IKc5=kGvxx;Loi|RGIHhwE|YBcNj3T>{SxCpKiB%>94Mj&7*dt~$7|sA zMiDyCDKXq3#2{^`cyNW9b1G}SeIo$3Ne~YyvDI2Nt4BB-+lc0n0k=)_g5P<{lUkjf z35+nU@#+5Vs6_9A^6BrBcLGoLVbNx8=W7=sbdKXFk3VH=VLA4Fa>nQlM`CTxl`jlz zr;!7PXN%I)xgGSEEV(+XSXKEGeN$D17i`Y2W+i|}UtFGp-VPc%Yx1L=xXNRy#RdQY zk*NwCuwRoC?>+=m0$(1#a%tK6%yi$x(m!tH2Vwr(Lxq z#FzO<;UW%n3c3WQ4=#og2t=kl zN7~1MW0q|=MO%V7DT?_g4!5h15Lcma0!o^N`jNW(9w~)Gd%jHpv<6Q-R>N^xAtA(2 zS*7oF9EV}M0Xk|XHO!V)@m-`o_L!CocA;S&@tO0Bb~ zyB;L6HPH~Ta+GY>MTI-`7EYT+$)o}>gtbhXk7J*Dn`rbd58avm=C5n#cEEsF;4Mo{ zobZ!*Dp^4e752m$SWO_#^#wF#(!h>xVKR8_7?KL@vzH3aEFGGCt?t+U1?q4#FVjK6 zW+z25lDSgoJmrfZ0Q=N<5J~~VY?0qtjT#tp$uFw^M(0%T6FCr*3b}S(^Id;~E-yuS zdFnP%4t!FIWO{*vi|-TwtP-j-fdjlJ3~@CsUgPrS>v`RfWqIQ1p8~}{4^7M^lmP7b z)kaU1aK2s}Tk!%PXYsFr#RulHzm$ex9RAc=zX>@_H#7f0w9oV!fm1#N_7gGOQ2x2X zP{K|&{LRl;Na#skvxVG zdkh{oGgR!o{~Gn!4tEd(u-~t{Y`1V}A?mb$en#cVj6HUux{@pLJk?+(v4YkKO*d<8 z^1WRjtfg2%;S)jO?XtkqOY@!+1fgo-?5esAZtRJtn3UaAlH%3?97+MFQ^(UQD3=`V zskCiAomy+)?T=Bo41Tki4zy_qH**3JP}wA;rqq&eN!XD7Y+=bBYyCLyBxf0mpGQ=P*U`P($3XB*>O8)5V+uo|h+YPeoryZ0S$G_5z2u8cY z#UI~YuN}Ghe7)Pk^D_?fwtzaBjYamPAecgekknG!M>5%4)S_ReSo9~*)PI*mnXJIH ze1;}_9ZBQytdsqcCTi2Vw>7OKBENPfhb6c;`w1w!3RF!`Egb^Ij%Lp_JAM8%xv2`<#(F2*A)(^)Y-{o1Zw ztmn1X8u#)c5^UwVC%Clz3<-h24nh8^lIr^H1l4`A-@%8shjT%k0VTl;PQwrOIzAQU zzK!qOv&L02*0{{qrg1|MyfidP9}`y|+CHulKH;$SCNw&)l);6jh{fkn->0#0c3p0s z^_n3&3gl3DFCRGM(MkdtD@hzW&n>ZgXyv51mjzq?_?jZf(#}1khZ0)z_0!o4r}5FZ zh9kkaV2?qei69b}Kq{5j$X4;GMf6VKb;MD=FXr*7p?(CiL307rB(#uFg`usv>V<=Alg5+$PT z=vF~yGsn@0KyT$cDTaXZlD_*W_(ZFCSCTu2ariv<1y{0?m0+J*?RHnKjFlJ5P(VgN z^>`mn1C59XVpWDKvZq;|mTzKrLoH9Aoy3PEhv47=B=p9O42gxE+y$wVJ8w?OVLFE>sCO9TsnT=&<@4na6G<7ylu zgq}IDtkD!3zxMgS{Sq)<5koT5APsMaTk+>eb1Q!N;h=WowFu_F^TbhWxN1 z$=2;-|L8XY2DMu2wm4n36ZW#3_M@QiX9cHC!37HZ7Fz_Hrb*P8d&#Y?ROeP~Yi1m# z@*h`ATY-(@mke*8++66&^)6ZQ{B#LK`+L9?h5A`7P|Vbrhn7#+Lq#_oxdgw{w9S_i z{>N{Y;o?@O`P%jM_D2~FcS?9%vNBM>nBD_CV%*wbhb@uNEC~P6u}Gqq?)vG69zRu6+DhI*9@k=h7yu1;tPTDG$=JR}eGnRcD9*Xta8NT$_5c>%j zVo0|ud8yrHpDW3KAwf+`0r?-0;PWv@5Mdtt`P%+P@PsaTaed|gUML-*bC_({uq@7w z;11#qWF_GOf;81+gug=Ffyt@ZUQH?Fzr8HE@$*-v{EdGFX$unjp+CtIma-KseTJ%v z{-_s(_V}n5Ly}o)?@<#w&3#-qI%kB=9_mz4;9@w=P4E*Y1V>h10b_5_D&xlCGAlK( zsaZ`F1o0ZlKk8lgxp?O=lIRe7{|;vq$fwSoGA08J4&<3%mU3%q?NCc3E>#d2@dT%9 zgPNE^Yk0Go?POu;*YN_ptD3@ZvO!XF?#k*y&^5UG1od&Ci0uz=nU2@50Y1*c3}EVx zj)Kz61{jZuISQK-$@IM51LF9&{=Z&w9n@+MDqkVHYCS3r(@SO2rg2FfDTK4&QP88S zu$l1}BLuXbk`;`7*PprCBpB7)5btpp)6enXKq4rf<>CR(dl zS}jc+ignEQY(J+B&?slvlgh}TODj3I1xDNYzjz48$}I| z-I;eaj5>%qkdc$dWu^d}tX19&kpy7SMKzunU>5a$QGxgFygt`2?whrKV|!q*F*z`FH&j5Z?Z7@tZbk0k353OyP?5 zU@grQbKqsn7FF7XwAz)_-WFw(q#7TS*t|-aCOZ00J&3=)1$Mdks4=LZECm}lqTS!Q zc8>p;C_?^AsJ6B?5=KAOVzJIx#@is1he#0S40H8YnT=r)1BOmMq zfwJS!&PNTGeBNz0=@f;Me|rne5e!f+tGiPv!V5vmx^4LHZRVpYxuop{Ua zx5LY_ms#kr0>#(Ao)c!k6gS`@JDuj8lFQk*Q|XP;amkk$+9?i=4Q|d&n>T1}6&|WeEV^nf==_;x%)lGM0ro3jWW>GgVC9 zTb_n@IanDf=zqCR40FIGVnw&443BpD+xpLaADu=m__kS%B29U;mjVbw7 z#&JWFQfv&U8}z{%)=GXuq}SEYJ3JnHf3TLMPNo=zk7EB`8DRu9WNF+#4ezc`e`4!F zsL)0d;19&UnR_W+{%zyq_=3rz&)G-9$oZZa%Fy3`PXrAAJ)usdFe$dc@7b*(|CBqR zcxvg`RA~gQNC{VeP_B8w2J39UjaELzUqm8e7INv=a z0Ra#(dAnxGp9ZW9BVXiT)YKJ2Le#Hrjgv; zlR@FJSE*PAIrwC#Rb~3A{i{7@l615yOnjn$)W~<;t-6lXw3O`)Y6Qm?f7Vp{(nOM* zIO$c?wc}fOCiQztgC;HWyedCO5*Cv}?=RGOiGZpu`3n_>6!z7XcR>-h zc9PI6$%Ox}v!%JiniC$=BKHSsR`zZf@n(jgUf|M}tGmjaQdGHaiABABShxGCakKxz zFckjzhRMaiN0WwOR z@eb2>QSu+#WQQTV~X2xfr_wd=p)8uB~rOvj$X#@_4KcK^UPtG!zc&3(@eXPS#A~+R?m8-rL zqeReQ?|SYxsQ9Wci%uk5`LF2HsVb{&Jvw4?Uorc_fAcn8gE$nk{N}1UXaDN6yVph! zn2fhN+erS2>7cr?gIb+((T~%K!nUZ)Tgm{P6tPE_?PW-o+wfHP_R`N|rqKh*QYEp{ z|DX-vp+L>@hr8mP672T_dTCNzYt&SW`E zfkwtFMdsH}@m^+~IS*rPOgkU{l)eAwzuI|>f2TP~dtLaGxQR|$X7s-_4y!RPDO5CE zUZ-L|>Clci{m&8y1aKsjz3Ru%x7+twrP9g(y2K17j+?UK+G>p4O?LgwSOEcoVD*2m z03dJm09@;AYwTXDk30^=r@qU9;6o zJlw=I`$4Sr={Ua5*;Fv#J^C$M{MjhYq4{MqB-_9K2Eni*o;EE6DfH>O|aA;GKkt;&);f zOR&Y?#1zy22}@u**W=J#*qzhrr5_7aT;9Dh%w6En->O^#cpgEMPreUioz;^*PiOmE zA>qt7;tt3#E49L}NTyac;?rPYc*>aLPhzk@5`@PKMgd7x=@R&V?OdGz+WpCKl%Zld zPQzUGD&iY1`?}K)6QIyco3uH}jO^U?n2I2j{jcIsU*QF19;V^RGg{pWIz&^CYJO6q-S`K^s2iDc}hM znv)HbnAHLDgqYuS+Lf9tgpWl2Cx)G~AF&lOdg0dgp5nb=#6oqktKZ8xmM-UC(Qju6UNC zJSg0Az{e@}_CN6ipPK(wnS;>rMu+faMfZCMCX5Ym-x3wNN%{1~J8m`&Ap%#b$EU&+ zLQ)g{>m%vpJyjWg1CO52vsYOiNmJvAAk}dRc95k~`!%Zb_KV^}mzsc)!f(3KjQ_O( zMR-VLUBRO%DhB=ROnHh6n0+7%_wbh-fmHT}Ida8kw}B-H1oB?`@?YST<^_S$AKns| z-jWb`NeUDv*BdX1N-IYNsiqn69%6__L^-Pwt+f9UIkO7v%f~sks#&>bH^?o3Vj&V2 z0;zLTC%xYL+%t|ic&QHgS33}lJ3ft*ZQYooLhVadI`|f~32o!a+VIVbu;}d1N!($b z6dBls{~8$@)e6)4rnizRGFEu_uvH#~) zo~)!)tM)VU$&baebyJIb)d&iGV5p{q(LReH)|8)qP@ae=Wa=eYNuLxIS~k~RdZ z{S(Y6)>8Pwf<6%tG#{y^tCw`NiNDxzp0|f$HFj^)VfEWL{$cpQUQr+MoPRGd3N{}u z;5yQ#3o=It3LL{@nj7`xC){s$Vu|`ob%9I=8dPuRqzUfZqK?TUz9rX;B+;9Fn@?(k zQ77<9IJ1w-ed#fhTqk{A*ARnGp%pK1`jM|gY1ORNES2Tlie=TL{4!zfRnQsq$nHd+ z&Hc|)wY-e6Cp$$`iS((L!oN&uS(a|?{y99#b>lk_b1w>CJCajMlvk>e&zBIs)IC;E zwmkJ^yZK37MRv+t7|sW(@O3;VxGRoo$AzD8!GE&=0gEqVu8ZQFQ_cR{c)O!-GHMg_ zRM=$G?swDgF0=R6Bu1_YOVn@I9%r}w8SNqdGx6bvSol4pKhQ@O??w?Fjn z)|wYq-brf}R+M_YTAXOV;qA4*ZlaHC{Go2(72GuzgZd=3mp;gSL?G?U7OAL3K$+e6 zjdNw+RgjW+5ufKS_dL#qz8NzgIIQJs`x7l%WHw0b5nuIOGV7kULyoO%r4$D!X1FUl!zcjnOMP6hzdnj#h`!2M-yxPP~-6S zg+-+8*0<<4OT5~0-D{exMp|U9m`Uwv|2y3&D7mbV_&r#N03g;`12<^VzBS0b{%n1lHA5ig_XW1yf8k_lXLT17TS40Bs zJxgl4MvP+y+(cKgpI)rC^Wp?<1p5N_kh(-eE6~%r5xTG9>m)46*Ok6bVW3ztd!>%= zQ>o7SmzhM5XHeXf4Kpj|Qhe=h`vDja%JdXWCtx1y0yXL4cmNM8NrF(_VIsJ1*@8G?-19WPAD0MW2xB4DG1;qg^kHAHR!TFfQL3tY(W3 z7zV_G0!&nGdH0I#Ku$Ep&5v>M5IK=K&8fiaHg#jpF#@WuE_RH52+S3DDa}j^yWsr0 zZle5SLyhp>tCs1@l9qeX58U3!(i+^~NCL!j4kPS~_s^B;@%Owot+;WA>tX1+r-Nn7 zSWeZ^%plM(aE4IKw@k7SDmy2-wg~K&%`az?Lj>#$exG*BW_xtuNr zffl6*!OkD{Qmi`i(q*?<^kdnhy=e{}IZ@#iY34-|1TU@o>Km2)h22u9cOb6J?YRa$ z9d%b6ETkLTsKd}#mm%qq>eA4#xS#-~-YP;s>q-ofZqJPGn}GS4>$fo>s-w3R{kNFJ z@6(?)F(eln=MW5J-LGU(x5ixgp>PazP9}Hut<~yU=ih~vvvDEs46{p zab58%-W^9pcf4$tlWNXT|993;QhaE*deyJC19|_#-zgv!k*7bow7%SmX$p1DpSR=^ zH`Hj?o_;eoO1i;hyk6ao4kI$LN7YBI0zMA0==ROxO5cZ~8{xeg?UjV~Y?S+SiZ8tU zjXytDx3QQPxEp$#e@-UL9i%cDs1k}PmKw3GNWJ@8)M**!dV3i}#hC{Vtr0 zD=h;uI4tbhgKHmO`%D5)A%KRJf}Ud=w{+duiFcjP{#SEZwSI8h+{S==vP4r&*48`3 ze>%D!yiZwW;=>HOCZp6BE+a%QT=kl4_ltA?E++&&?13@SXmrV+V%VN0_7jSgaC4M4 ziH67-c#*M^=DO*t_TI!+$VQ2PX&{t2?A38V$XG|@rl(jbGW4u<855$!MdEx>JRJ#} zzgzcg5-nU@b{GxpY+x0GE-bG!VN z1rCg(6qWjiRW6Kx?o`%w;$F_yf**>O(E91etwyI&+Km`Kwc6ILmlh`v(iSbar}eQ) zpC;ZJ#|l7Y!f{Py#Ab)U%3lKaw~x&UlTO%NPA4KJ7$qY++cOc`zrNn;BNfUYXaQ5xt~^m=P?+dJU;Z( zuL0RPZ9K??N5J$F#c$kr+O79CD+BgY2@b|HQSb0pr=FoX_TcOBJ7%b~_RAvJ3V&R@l-JwAxDWo^#2B zjkb&oZ!B2*4p(RTHxA;xd;s0&E^|DF8K-!LK7i_L|JH;@ai(oU%REzj`^RZ-?X!P^ zOD7BAY7s3n{E&)CoBA%A?E$!-YAYBN2y2Y+VdV&QFn`HArES=zc8TdS80^$3?QEYq zn_Id8)6!Ap|DeZg4F5`1VkabAvF5q1c{hPb$Xc)WI07Q)z{L9N4L{y;jxdv*;QadI zYy~}F%%Yf|O`%@O^k-i(TxVl>%^blb2@#%_-LUfu6#tFswOvNr_C(;-5NI;RM(P#Y zT?e`wOK8RtNguLr^mo*E)=tICQT35Vlzzy%GskL@ zRjxAHGCAk!Szr*PETY8MNNTk7qk~nh?hlrgk+(A>+v33(h2RqGf=6(YplhF?k0#{_ zH(Q-VAQOf9NArzeLGO$>*%k(KO8@|3#nc)5A<{!!5SagALsY=`WjW*h#_ zb+;xcWt6vi{7K*8R}jEZ26%o%BqD1)Aq*dLB14+G?-{HFR+6-8n;bsp@_nHmPwF)u zAGhcA@UbLt-g0!&<&EQu70XUpUGXRFQfOrTR_;PnZ0T9L#f0+yWcX4K*6=U2kBW!8 zPJC(233r~Ij#Q1`GcyCem)-bImg}~aT;mjWPRU7MumjbPh)G0;O&vNnmAzZ(C>)Qe zIKai)smcP!o|QA-yihD(oaTE@ZWuo4u=hr`9~Ogrdccv{jvxn(^}*+IjXcDM)gfto zDuwUaRZKy6t#7RUEL?*%)Yjs|mwcwdjqA;}SM5ph9%tt9a45;1kvW#Kfd(TU!Rp71 zNHxY^h$~na%LpST22o(;;VX?oGD11?G=m~#p>-4{U)qOKMQ48+GqesT3u)O)TclrL zW<4mg5W{scG;UBW3;y!%)e>%hh0?&2B@*1K%3z?qn{@L`p-}3#ESL;kcZI{15TAQ$ zue0BJhK#5{m!D}~vHfB3f}=4EVGT0nMk?l$x2cVpOFt@t6U?^962WvKvoeO|R9Zre z7wNyI&?Kp>-Fu(85SBlvU-iq#_>X&kysKnkR;B@hO{?+Kpji5sLT~1e*O)hSK%q*K zzbyZRf448O9uQlaf*Bmo;l0S7h8!@r^GotE)015_wL%RtxFSc1b!@EU?WAZz^-)A%6%_L{+iCGo7pokf$IU^E27N`MQ( zhQ*tbu4Mpt>IZ}6!HjOqn?@x&y~7E^6!9IVYG`}o3+@_^x=HLQ6pAKC=eKw_&z}k6v82 zmb;g~l>tlzk@l#fXUfS@d^K}^V6uflVBSu9&vUTm2Q`h^S;Z;~1jP))Aoql}4)JMe zD;bSvNH?Od!7PwzSBT!`$HU=B*rcAe@ORK2`=){7>(ZTkuD}0oeb0+44odcj6@JH2 zS^Cy@zE*tO?IK#Ulpf*;A_-gtkwu1x+YzgojJ)e={<1;iZ8S)`1B%E>o-H-L-9PT_ z41#*Gkpw8C#Gtk|30kL{tuW@9Z#bgt;6_LqB&Vyhf8FKx>CrD*@_T>G15vsJJ4?J< zS=AvSQ;B}Qx;~KWLNEbRovN<(;)O+NjGBNXO8LWU{&w}oiZfjh0Hg&VqhZs%JQELs0j{{g6jgH<`frGBgqg zu5*hatHbfd&n1X~Ye_TmcHWWuz;)np zMnb{|j#Yq{qPyG?e)g+P$*x&c`K1)|U6=qSEg^>lSS8cXN^a47$@{c+c~tyF}X~&4``9 z33y#|EBIamVh{z!BER57hXdcBBw+v+rD6H)tqF9+*ELRm+aDf}?oqnl`11+=$LM#O zr#so*bPhi8{Re^cm`VbM(wa!8@HY9Wt6r2X3{{DXj0DP+)~9IPXoZJqshU6i7GS?;xqI@pX7fikBpoTD zPv%0J{lNBgFtW{s$&#Bzazj=*5a4$m-wG~ zbSQ^z^t;O`I3w8PrUUNo7GULW_U6YGPtEqzMS|3k21YEt&`}QV_~FXqOj$ZyiHa_t zT)JYrhP62q^+lFcLFCdBtu{_BoVPwa=COmswXNVugNXP>S5fczL(w)SrQ^BpEvn$Z z$S^J{T0X)Z(N$`Kbj;T@6$O5?nXKuD^4NGz;@W!-1i?DLuSEoE-;pM#2J=3N6Q4A8 zi3PGP4NO^y;88b7l6D)m9_rEx1h8PwaT$WKhktMm?2~eHGoNKfs?|Qts^Up zj~Ww3t=VrrZ9Cx-!L(d5O@uLo+D2#BPUS|W^zAB{w`^&p?I0-Fd+PLN^_jX`Epp7W zb3nXWM`#q$U2=XY8rIv7hzY_4z_L@a;AMO zU(}6+f+spmzVkjWw_CkA#W-p}g_j*+NCx^6@s;hSW*MQ_)ZPQW;OguM8Is(7+fT&| zYpre)Dl-j;S~h?PoD{iJ-I1w2B3z#9%Kpy?^5a=$m`%Qg6hvX-Wg>3rvZK&;qwq%> zoKgi&iS>-MW<(X}M5na?}BN3@WQ*J_(Plb&2gyH&|ZGbhlSl$-&6{!a8?%eN0=F6 z=UJuQut!f0^>}Uw1p)_K#!K-Wh|jm(G(TMKr;dfOsy_mADtAHRcIU@y-tu%s4lV~? zsJxap<*iYIJY0V{oh;g&0PkhsS$0ZXq?6>k_nWC-kFw;?3!Mo!=UEO*%WFFK-hdKi zmd_+aBz@9GlnYBSd@~g0^(p(K*7o&2>53(O#v|WwALaw)*uvbN{9FEj~p8xV%)}hz-y%4SG#Z$%+Cpo0{WIu0lk5}Ml!PdKxup^#H zZNAM@rYmQk0=Xgg{COMsJQp+DluCR&w%>^AFYZTEi;yBgg?RNA>(r5sED^&O%12p4 zYI0%sqohU_EO%wo~Rc<{fkG+G*-ho;AMTaOHzv zsvsx33aIfi2XtCDulg)ipWPaW4c>Xx!!?_!K!oW(hv~T-b8}Cd4SOljSmyE2It$fg zEpA{G*Yl>({Ve))NB_k``m-M%%@Q^hCNcM>t|p3J_&lVFVLVE3`@{O`b6?l1KPQO_ z`pI&dDPz>`Tid$z(jy{Nnj>x0nEUXumDQ;$YFL}Hac8~Ph8=HW(B4gC)6^o|ZLhO$ zw%MFzg>9ag-f!Zt29qrIC-=gV3O}8!>c(rxKYt4V|4;Dl*~`f(BjY00U$J5dz9#W&p_iFx#${v| zXL_Jt#^DSpuXB>RVqOweu1_wvA%m;fn3HcQ=LNBqeE9x(wf!|(rdLJ2^yDjhW_l25 zcFJjkcJ~>RR!ErwIYtO^d67qOja-*D`wE%u9CG8c(JL%?aM9PMZ|3*y0-@zu&v8X0 ziGgZ~nQ`QFRN`G{k;;i(hlRDbW@$o4ZSCKOx~it8<~l<6S32XtQcvmnvt@nllO-zL@!?1Z*D;nw68_TtF4;W>Q|DONnN4$kTzc?)ilx_KYQvHS zhBL_+DXQtHT8JLNGppTuMgPRvQ>fe`(}a;=V`YRzO>~N3t=?!%?MS@X66>Z6>Uv7p z8+@-5WSG}uR;3flY?|)6S2z8XCP`js(4VN)S%XLPURmr_UQ=SGxkh3j49F7rzW2^# z;XA2!<^!gcKkF;UgFMY@xQ!WYt26yjba*>C8}C6mMZ!Zr)RXz~r5Go9LtHW>YjaJ( zwvW;@iSrpZy~Ta-uv<__>(H(T7XR43&X*<3Va%xy}|IcxPI zi<}E-ozH!e#>$kk?sP9_c2t+%1pKYLHnYym_^G)pJ*m*y)GdW&vqa0ey!Zm~+Sc}$ z-|K6$J2o7+_%M2gE+)oxAq&%yVUwY}4_bezR(Zk1r`pxlcjor+ItkTzT~tN3$xBS* zi$Bu4dgC5nYM>bA%7ckwc`F>ras|8G%4>zrI`Vost#V*eE-ybWgp5mjwl7#b6Sp`AFo&KqF{N z!)|ZC^pX?4tWVPZ&ydSY95^pGS~WPLX(h$5A!r0Hcqmrn(XGfR1jN%ckxx_ei+2p? zJi;{u+n!tv@=gQw2tMA+Pp{%rk|N(J))OhUURBiM8q}-8>Kgy?9BjXz+!#J{qOS8W zRlXsG)qm|z>w8Zc+Vbe4S4x(s=PM_qYa}OKKdv&q%qx6MpjCp7)lyr%3LeOyCQuEA zlOY>P?My#>*g)y-728W`$j3J-m@$adOumOkq&>e;wo1Ze=o_78m#O&4GQr*MwXe%p9ST`~2kD`-n+}6{3EiLfJ4E;{fyUnT zWS2~lff^o;C5143Q`G;SRHv7hDD?Oh$rAkq#PKl%}uOY<;=r?+fxtxl7@&-2}zl`P&q{iw-o#O(=fm=Lqx3mHA` zxSFmE0n5D#y1LNSq4oBo+KOg)P3%1f@GKW2!K0;4jxQ0|@YQChkH0!;!^mh+({bu2 z$D!zY@ee~gPKMb4@yUm*QawqXTrfyx{4-F(XX}ghAt87aV^J6B_ri;Q zV7#663!iH{b#7-#RIY%Av8)zlTKc#<3Bs)-&wyz{TkQcZv1H<#8uHX zk=93JK^+)5N(6ov2sXIk0kMXFT1ZWEaP8R(d}T0V@~1 zVe(ZXjKMPvS!3PJqDdMM{BE`hyE!-JyO~*Dmpc-12Rv`n6KpnEHX28IKH|*&6t-3h zY0v^mQx{S7>J;DZDwaB$Tp*H_q^R+I4gKL@%T0~9bNRdmBSkr%mE+fsj~l%1dM+y= zUlG<2wO;*5J?xth8NSq;ekrs^*Ufo^(YB4mec8e|dx-!VD*E*m$og{8tHAMv&w8aDu2{G34I|O+#+hpS!fnC@DVjpFI(^9B`f&lo$oj+IcV4z+jYnsYnH~yO)BPRho2^G4nL)!56t!2r9?;;gBn{Tf49ajGjx4*!Q=U6v)lOC z>=m~8*n4n-=x1zZ)HPgXaqx;kWYfg|P>xL9MQN(kaVd3luCN>*5l2FLy%c>GVmvji zEx=x?GuQXspuNFmG|)>6w){ZbH+! zCuO00?Y5=teolMo2`8UWgEVKsv_fAZ_Ja+U{GpCe&})=G?29fox~2DEog@!o_qj~> zuqC7ZWDaUPX=MERvOF1v0s+@X;&mWWeB5PSm%D#y8{|6=cyLV}N!8STrFiO4$ol(6 zBp8v?Zz@|RSBzmCiBU%zr;(Y)v*zPJ6lRF|@dT?GRNHz1^s~HnpUSD;Noe-vT*(x9 zdaz=BcKcbA@2;Q5ys{C3O`G}7hUk4U*vm*ou|86gjv#}&dwiFAA|ZUG8VcMi{%MLF zCWUTKU+EvCmK@n~G;ot@1aoBb57vdddq@0NloGD#vXE3Y9=0qC8f1V62R^XAbKz_- zp>0U})t#xErNNO}h7Z4F;=f*%heTh4&uG`Iyc2eTaT`%J3hmtFyre_wA=8@sOx~* zIFX@UhWX$seMNla%@#}Z9#@9A?|G9$xAm)5)viXrZO+pO$l8GoUd`x$P3j(nw0+cp zo%B*D3BPpasQR(LT(;nkE9F?#9Wm1#E9udKf{&T#u-}`BpX?H|?;nHbx8jO9U2QYN zQ7;J0ck)*kPhTl0GcJw2YWh^^MIc-TSAu(Nt>#zXp}P%lf6&-&_G$dXh4RG{q`boE zHeqyBP|}_97aHtli^)m~-WnlccrW>i?2-~jD!W`;^tWu^!-yr;JJ$bf&CXH2$^Lc* zlG`w$dVm z{ZDJ{GSk|%){N{E-6nd584~;}m>zh&tAQ;0(py2>@8mi=ez8`dPMkkASTGj1(2$*O zf~D}4+ULcJF>Pmk&@lhTj0}5^oNm&{^2`#n67Cw>7$(ya88s)MQOFxeDlo7 zrBxCHr2r$7mU>p>Bmh6FvN@Lh<)q4&-8a{D8JU+on6Xdc1qZ3N30h9xLRHaL2`~m3 z|2*0UdQB}Vhjl3oG-MC<%!5&F@UFMBt%2OT+7#?V_LetIfr310~ zH;%GL9ugLc@0P|!iqfUI-R_#0{X)~ut4S$KG}aU;M|D&128BAALL55wE-*MXTD*vY z=VN+hCqdRK*X|WBbzS#)9gdJEtdfnjjMLN-W}}&9?95H@Z}dLPG&+4x(9hE z`^Q~Q#ky~IEBWf4hhC;<_P}Faa8%U8f2XUG6)Ck772;XGT^70CrLi4toC))L0Es;E zj}~d5`*lY)N^v5lSt{9jzFuCC;~`v;&3^T%T$f~Avv0!q7kli=`sUpHrE)%F4u%;( z*tSs&dzVBKI&1VlzHlcgIQ`-))qWGU#&^xv9XZtC>7*sUy-OeKt%d%aRV{jquz$qp zXDa>3OvpfLT@qqm0U3C7r_G$j8To~Zg&bue}gPi8vYMWUl|Z( z_q@HkEV3Z6bVzrnq{JfKD%}ktDI!Sk0@5NS-Jl>y2uMh+ASGQ=(k&_7``$di|NG4k zc=nt*GjnFHxn^#-y&4`MmgaBTtAyr!d5v0jLrlum)KU<2n&Fyvgq?jx^&HTg*)#5EIe4z(K6Y@Bd_k*{-djtPO0WH=- zWJayZVsU$of7h*4rRV}beko}2@m@XD6(-Rf?IA#U!aeM4ABE`zJm$%a*0AcFxSO${ z*|~y?pB+!Usb{ru%ZzztVDR1W=wzJBfa5`PW7jXt{9bRN#&dkH$DOmVBK17O@*czY zy*&wq5<|MbqFvv#r|x62ox$o(+7=_g0Ftm`u?GF%D1M0Npdr#SJYbFa&rm|N21|d= z7h=u~!~#9DVUGMt+Y_s@wqNxE2di1Rbz|oOWJx!~s(A=Ot~tPLJUjcnaZ|pm7|@_D zZ^ZaUj57Z8U;~2>_}@!IAUzsLT^HZ4O7*&TLmoc08y67^qHMh;5Q6|pR!(-2lXKxH zw%zVwxM_^pzZpmhZu==8Y@N9C4j~2oZrIoLL63^y$@fcqWee;i5l;oiT}Y1O`Gj5AA3$xMkw)yGD#<3yQk0+vR95bdO%2?)~8il-zLKeR%sQ zbU6_EOVujy=+g+uaAA~xJ9CSQS7{>m?5PTe?JBJ)*Oy)v6I3iqs^SB1;|2@5URU-7 z@vg|y=(oTcef`Qv;+F64A3(N<&wqCd3kSJhvK*CbPSWJX}#n`4Qrc&^l3 zE_OTg@gKuWw)ntJNG{zzKv+bad}V75Fdn)D1P)3tC*~wY)&Gt%JLS*`@^-AUqazrz zG4Ox=Hi1WbNVQ6uMI1WHYxS;!6B(v2$O0mHW_`z89;@-eq$nf!*0W}dR#_kwuUdm#Yz{yUOkQ=!dwdPp+pJ(7xym+ID9nZvy?Y~oqdPJk5Ye< z%pBL3ysH4X;@&xqkQL!)rky^Ab+LGXH~W~wRSuug>1QP@SKWGOb!XE|@tq3y$ySOn z7_PGjSi8+NoOrT0DK72gf>Lr6QTB26&N{CLpmOI#~!WTp6z~)g~5D>Ykw?jU;QrOdF}$Vbot`e z*2VQR!V~;`HVH^r?E+s__d}XTNvPc<$9nlY+-< zb`;8WTq1M_TzxJ*U);Wai=csinR?aTcw{uswf;fvZp=c-;9ONG)y11w1=fd9PZBEG zC#&r%PS+b@A0LR;88zB`Dy9SBcMeR%?gF@#8LHEL>RlOB`b2m^Vd+;iKAqKS0qKPn z{_Kb20!l%faJ>v64iyGmVy`>ysfrYFTXETo0Hp}bmWx1ryPo`-NoZmBrA8~vvI7A^pv6^MAoI6ZqAWEyvg9q0*6FAikrE>^8+!y|Up{e<9E z>6P7#c#k&${62@!qaeWfuBpLmTCSons+y#y$qMx@={7sVx(h?gM#+1+2FVU{-ZBMf zBMx`8z65M(I*`X4UF-GY{_b}OE?ttfXRhRYLa6evB>g8YCnW$m53mCoLOqTX;6LgB zcws6<^JgW|cKr&6k|rjygV7&~MipRf&y)^a=3J-%ttRg$#9k3z6T}hiz2j zSWK$|g5KgR#ll;b#mF4@wQit3RqS(Fpz|N|?khQv?OhFWvbBC`FD(LiO}#)dCWH&z z&diqm2A1VHzVR)yP!-7@*WupV zkeb_i0jtDv+J-6U-Jp(xF7E8{6Y<@{pD_XMZ}sU|d;|~I3ME;IfSjp-a`*?|w98vb z@k`Z6fEOAt3q)MYlUev07$q}oJ4+N< zw|ngf;O;8%)RP6aJkkZQI~)pPQO(2v z@gpRsMPeP)Y9MuZKYYOAc-Gsvv7)g2Zr^a#a5Tfl)nQw`KS{r1d$}O$g*m@CmAqjm zWcaOowNC&vR#HIr2fVilLi@Rmhd81e=m(BX6ZpwQW`oEpgFHGWmfSaT5c=EWI%v{Z&j;Xvy)k57?!KGY5-HlPNPBOZW!N4Ss!Vk;Mb)ZahkX{|n{({| z6ti3}4y1rsPs8fp1B2V`v)#i}KPfQY=V>>6WtHFdtQ|wnOtCu;h)|`P`fJAjU~f4g z;8p{hL&+-g^%$Fy_i1`K)~lSu#Z5ipI{lj;c(QI5Xr_l8eLXu`9^TeFbT|Y1wm@0E zO^OMc1zIngwN*EP@jw^cmi&zx;&w$7*nRFJ}1(`Ojfd zlzUcu!u;I1?838wdheAlQ}wc%E5RBMfZ>+F^t?AWuDguCwULtxpGfoKY2;4eDyP%w z3)7GBr^Writ{br>xiiv2fT=LT0Uq1E+|ch&IcUpjFhp_koMA}BAf%gNvj@d;jyFxh zYlOG$AAG`^MxF_Ty^K!=A?_8^4@nl+$h{z|SCTO0DRZx9$}f11v`{y20Mk@X!xv=N z|1si^&;9u7b^5iyy%Pki@jm`H8vyxm$8;t&V!VNq^l+J2{)&HwzICxu6C z_uu9B_C?=An4&TZztx$(#0(|jz#1UR5ZkSp6Y06Z>QIq8yBB^lF;VXlBy?b3na{tYgGVMHZ$cXQaT16pVdQXE zY^txP{m&H`eW0F{RFXYC)G7zxzBU?p3}AfRPgX5so?pA~wF6^E5LOzfciEue7nJMJ zsb}o>; zr`JdVJ}-#Z#J{p`yCOqZ!!sLys11|H6QB#B(n#@HxIDlElm9qrIWM93la^fmH^4Xo z$as4Ko>DcpZI0gEwyoC%s4k2@qx9X7mB;WqH087SoBtfIp@je6Qjk0cNF18TX;l>R z6hy2~O9vuG&qnt<{)C+pPbJw;LPJC&c!aZVh{oyP45m6`mR=LYm;cw9;z?rEF#Ze3 z$Vhn0fARUCSX3lEha)kL`&#H#sZs^Zt0w>2*wetT-hFix0zHSVzaG{7n1bVnoxDXH zzF~Ugi#bpg7C=?8!>ftkFd-Ph_-UPD^L+a$|H9c!Em-?Zc>892oKp5K4X0^7JC2Kt z1vc%qyetfpKk?c-RJj`4mfrt?;WEjmu8(I`3;p#bgA zXeY8%H_AuVS!WVz-RxI;d5MO_XEK4nFa+a0lpR3{}q^)d-ie4x=zMby!O)2K6g@5{0-plY+BnZj~G*UtP zmCNeolvoig$MZo3@(wyKtT<kc$=vTx;+WL!G2X;# z#$isB(ktf1O;&n>T|bf1w#y)6i`i*T`Mrq$mUY0r?$ENoIHAb`op|#874gR`@+r*? zu*x&4K$)q>nH7RqR76h;N{ivQy>=RF7k>G|fG67?v_=}w#E-ab#8)!A0~y9j(Za9Jd* z2gfFC7_a+7>wd(RzAxn0wJXlt_CgV-P@{YC6@(t3qMxSww-Js!Y`@9IBv85zN}jhN zRSY<@rzMk)=~7#^Vs424ZarQQWtyw!Ic6)JxBbH`lKEol!_I~-5zpSsP-IS!Nr2j{ ze4Lt&8lCmoxo${vk?R1wg>U=Y|Bf7qoR^R|k9%B9$ahma@ABemK79D42ykNXT@!|- zdwEavYu=GkoQsex?8epAs=~{^OFw1y;GEGmBIx9C6c!`)a43&%M!Vxy7cJ4@L^4$|_TjPNky)Xi>BGm+>BsE&XxqfHe*PQ5-$kHS^KlE>7l0V}j z2&|2zs)Z{7dWI;+i$1`RN$4uBK*NcMefP1gVQ zX5$nwL-R;FaeIj1icSvB666zsU>$i0==#{7&k1d!OP}Mb+=gxoPZZdqD>NIZekw3h zfJM&5AKi<{$O-<)Y&pjCkrj5gw;m!CrKP@f)3)B^>=1X)o4{X0xDa2)C-KXVj-CuP zPZHxG+G%JdZI;*k-iY!p*}#cV;kcUAEQi0xIs&0-uRh8hu>{tVtzg1k|98LD>>hoj z#vCd!pk~O8C-3VI11dl;Si7-pL2Q>uWE(GT>x0xsv3#I&v z$Gwv$3v{Nb{}a;OGcR<*H<)`Ql>1_HZK?I>%C^Iukc5+BVackzxs*gF6gpv?Sd;qv zpVV*$6kHDrSgrQJ9H&;6_XP8^xs3D06p7vG^)m5PmDW#1Nc<&-$7p{@Rv_dl=tg(h zP#mTRY|KEhp%5ANgXSO2-fJ#01>!r;>U+c@xs)b|IUe|q8QLDk;3X*$OymFOcRkY4 zGCawDE}Utbu(o=3u%DoD1mWyKLm3D%zJ})VK#SiK2aVT)D&Vc>CAcmdPQYw9!E9o=-N(FjI4=2|@$GRgf4l$jTzYY(Mya;Wn3DfUZl+a$Z z0_fDif7r6^bB=C4{|%)4OMu+EIg5x~pk>VSFTJ}9toO*U#lMU^?HE#ovxI{$ z3)txmLO2kQ)hBKp2fu1Y^4Y8q3kqE07Pe>Jh}X#HwvF`Tb*?D$|9_3nm422`{Cg3) zsbjk@*~X|~a@iERz!)zBmQl6m{SZ&%0b&K6j_Q0H65lXlXQWAhM=DuuGA8)sQm*G- z`Rsk}$(XdUW+DC09jwNqQ-cgr0&$G-hExA46UD54uW8>QJ?;$KCM1cR(%|DNKbXt~ zO+QvM&qf+4K4ug{)n`A2AGH(ll^Yo*Fv$GWeDd6{xCQX}Q_y~gO3bxJd+TLQh)$Zf1CSp1*D3o^htwze!R|2u6KU~l|=gMkgGt7j{( zXTZaysjx)GMQ&6GcXQ{&_q9d~2@tQddoq5T8PT^XlTKm7pPejL4j8R8%jdl=pp!n1 zv+`f)2TQdJA0op0mbHAiIA(mhs~ggOWZY2UdBs)E%r zvN>S*4i)tRAZ$0ObTfmPY^4|ptoUeM^Yq|D3>C?^ zO@}BBOmXC2YT)0+mxSX#Gr12Y-JX7sYKUFI@cy?NWiD;+d4l!sHKqfHr#i?vz+v@n zKOk@$ZW;l2_K`8(6OBHHjz#9e5R>N(>kw38^TgX1=g)9%$N8KNujLj*jyn`10wu~_mortf;`plCTs3~` zq8iRQ#+8_@F;V*9Edmw!p-1QUPyr-w3}#Qvb$A+&$^3&gE?8fBl@GACS5v;Jq}K}P zNhjs(vHwm8*eWLk2Z{75rt%WAWy%+}GZti3AZMQLu@BO>yi^53?jE zrH~#P?9flK36lzE!d%j!k0;&r56A3X)|0`0(VCIy9W`dd%^#=O_wNgPH~5E=kkU&# z+dQq1GhCgZQ%|WV>DkrMKi&vK0)tm^Nv6K)tTvq)tQMw5d~QLTJb#+Zx2eF{3M zTF%kK<(0-HU48n4sqe7R`ld&8zXNO~|2Yg}ij0i>gxkCu`?zB}IFtXO0vO}zY}{|6 zO#(gCM7xaojHo4;wKOnt&?Ye$R!6D78M;vNJ@duBIO$V&^ZUX~BvU-~L~y8!%4#n7 z^tsY}a|xped&7oD-!r<6?ae|=@fq249Y>$Z6ESEU`><4rRSGy@W7D!>6b7aRy&Eol z?QUu+ujfU4g_#WL_*M(6B~0wF{_gZ4-CmMN#xQ~7B*PBaiMrJ}JmJn4Iv=+Q5e}YC z_D#4HoPL^Kw_G|YK|Wyk+~cZaP?Y`d=eAN$AT^1g_G;yvK}uU7!`#vf(d6in?V)1# zxp%288TWH*IP#!4K+3%8`2B~nGim%w-wVvwP!e<_0EGgy&Wd1}a~pS_%tn0p9OpI~WdCDL04^~<+} zqVo+h$-hUnEn#FQxtre{|DxLHY1{VO3-FKLtO${g95lJ}{}Pw8NYpq!yhj2x<2WD) z%!rjFN%9Iq_xx7nmz2zE?Mz5H>a~X?C?@ec&H7jOz3x|-JMVKd&`@6jJx^h}$Yiz} z+{w-w;a}3ymsVbv!ATC#ai0NBode^Q6-%tVaZ?LAi!O+zq8UWKY41^$eQB=NdT6TD zQuan-cQDEI*`zq^%PhBNnZ@_Vf7Q^uPnqS>qL7azie!i+^(BEOT;%=P9O@!gsY+a8 zySIojX#cu(ei5d%joF_leAHwn-3L-=qI7%qTIr(SUqd)BpaE;qcMR|F0}-QU4G)1>^JEv zua;X&_ch=QVo>&X6vooxv8hL&veknsd|cS&8Hu2SmhfJ2S%c9>*GCzmm)x7FYp)$C z*0wP?^D8h-^)PlJe(H4B$XDpl)Uc33UEZkdoiMzL%=#1ckE718PV*Db>sA_A*L) zix^r%x#{);>P0|TLF+!7_A6@j^^k7=AG=V* zD=2@kE>-aNp)X}Uy3U(nqKNUFxBu^d-|dzvW}X0tZXUZpW|Se)U-v#uV5=PuT|$bWRwq{ z{qVoJtF#;UL6e?rD7Xd~WENb*=}6nnGcLBkbJEM6+fY%WBmR|-+86#j7Tv-V2`OQB z5W{NTRLt?6&^*uL1MikJ3C_~2)_dJ1+HUSu>(EJSyMrVol?|#9fc_TKKj;lhxIK%@ zHMdOQu(UaCa|)G5>O(@+nS*a~3bKSVCyVu~^xOGmw+J|p7@fva<)aZv$olTl!Wk*{ zlk&7aEJ@#z|AYl11_@0l5V7BDDEYY)jPGLiRgp3I0JA^ex6{U!Bp=HLS>GIJYi{M_ z7+Rii!gb2E>DOG@-(CZ;!tB(nKF0yy7bH;eswa%gBRY<|U@0kJWdJy|W1{@r>^4K!JA@Kv2Bp<%4JGw+(BW7jW$gZEI)Dk_Iu zWcwgMM=l_8I(XrAR{d!@YV+-VU;9tx_1i;y(J!I#jPc|se=jn0in$`#>!?jtV8r@; zoEyz`t|%%*kUgdlYrd4=ndxYqZv%Z0-*dqS^$KV_RE+j<@PT`rh2N&6l&Jy!JthNK zn#kV)L~=3#YN4r=F~;Evv(p#O|B+BJ@5{}IToAO_OT2ibgIUO*IDv5y0%jXA<{xKq ze~ktY=XI$IFFy=aCjLXS+O<1Cn)XoLKj+gGX0&CANzDAkyh*%O4Emog%SB5W)%EN$ zwTkvr9r4?XN72Gh<*?M?Jt1g;+>)LgEQwD|)0yw2fH)t8+#?r|I2)>q^~mSkVZ@b<39*(}JCs4!k1O=tr^(~rA26AU36q0 z)dQrA?EQ&QFeMBKX7Y6YERt`9@Rx3T>{4SJ_g}8OU=7fYLhoTQ`w-`Qr`1W8Cf)t1 zVnRD0bmsGIS0I0Ky*j>D@Y2LGZXkykz`_m+T2?+s@3_~8+-HJ6k47g8F*7t~P1@?h zNq@Pd$lrX8>!%Wv$cF&meh7kHdr|w3d#y;iy0NZ5w*ee3M_yW%s~*m=2Mv<}m(bM$ zOknSu@*-sDJ%g@#ua2>$(|`qhoEEjv_MKC~6IR~!Gb@Q(z3lp8pAAN=f-du>%``pvTmK!Rw7b@WQAZri{Xe_lkO zza}Ytiv4%RcC{I!F6LI>ai0tRJREIbtXTb=u(Hd6>QlnmKn$~_@27>K&W94ulvkkr z7NSMsuq^RhN4C1fd*F$7VWjfIAC_(`wFQwdr1G1WmE;hnW~;3y#zN#_57qMYBR&M5 zKXXXQ{F$ciT|;&kBq`;>RYMS^P!cmLpn??VH8KxuK~fOpk279>}EsJ7TbNoIE}sGO{2qsiAaO?ATEV$6aIaUyNF zcv%3#KcZ;CU;MJ->+s1nIN2};;bl@@W>%(!6nAvxS21s?-OW3F^{b>XXb}Q{u7bAB zJ;(oo+RWVZSw7@pGkv!JF!z{dt$jhAUz8{!UM|(`ynu=Hne|+BcH6ot+Kum0R&8WX z`_?B5!CI_p`as~H)C9CdN?z&1X0nv~ni$N8W$`LzBI|QN#GYybW4tKp%)%%jP`B*M zw*19AVybDH-fxa0ADm)oe8e@Z_T>}&bD2B`JRWv?61q91a9%JC(}HF6#dZoRF#8Sb zMPW&q^$soH`Ss@Y_RIg^fKBhD@#pLd$yfrjJfj9=tSibaAaNl<%=l32`;O0Z98h-x zn-=v&3+|s|szK)tyDC|swb+$6L8o;@t;ITdAN7orgFavI0{CUNACGDnR-U%OABPCE zV#}i4pk13~saM|46yCE8mV=6;=x27~jq8jKY5r63suk83%HcGynoUYEZr^|U1)$i| zAz9RtVp=e^+c&912q98RzunR$JMWl<9$5QJ!vG;l^*IUwHmDr;}c4hh|GfVSsllg#02oAy<|?5^Uy;q6~Bt8$J+7@ zk}XS#Nh7v+C0qs3=uvX=tYNz#Xw&|N$aOr7LO979-VPre24=v z-l>&s_!iEUi$hG!xIBq{ZX4q$hZHLlbvnF63*9`h7m&dTQs_SGjhUf9>!-pvp8~sj z0;}u=Gx9k#6&Jaui=$ny@xjhe2OTnM@%9B2z&|u7YCm7dXkp}}M;%d+9al`;-ez|e zCSuGSM5>kiaLEDtC25F|akc0UocVga`_aiKSw9KgD_ZIK6$i)-Wjtta?BlaU439l` z3WJyMpovUX&*K(?Q_grDk8@+!7{3uiC^yq>5_6&a!-I4r&{5VZx3V6M%s2Jv12Iu( zwXs9aGI5k@fw3cI?^RQqEJ#wRydaAN;5?OwB}u94acmsJCm!Lvelc$QkmE(zqM{y&Zos z_)UJFv}n5oD7pQItG3X8cCExE==}fzT=zW?Ycvdrg8yrhf!se?4EBg=6Bzo~A3r5R z!P?1Qb#7!4_;qQGmBl&XRRQPw(#F^IK#4{I;DhLAJPlB~QtmhvqzCo;5A#2K4<}7sp|XK5UOa zkuhExWw0wG=k0m8W~UFhl`lQIZlYi@OxdTU{S9jc=5(PS3#TK(;A7hH>KYD_4#M-J5nRF9)>O{&F@M~o=nbdQKSnBOuDKz9A3XY#WM1t zgE4LWx17hQX7Ho3{@dAaE*b-+oipFN_=>`=Z9&kseGi zhcE0JEFT@abU6Y9J3)()aPY7IeNe4BdgEO$y8h4( z(74Z-c0#Jf{i|%p0_uU!%0O_eihKvULcPbzV~`->5(DkLCu%ch5o}QUxs$bRJJVv> z`|3ga9{xqiq;y^H^;<3a?CuQR4G92H`gfF06M^kJ%2u_5T>#QPl_(W5cP^-vfa;M6 zg>T~ksaepXqI%r!hGS3&QWWA~D_rxc;{BFkS+X7#=Bi}o&o`$(stM0VhT3ZWaIw6_ z(-|M6ug!)$kCuoAoY~>%3g=*9f5`pMB$4Z%Ow0!HI>5sb?G*K{E+04#-{_pm(hRO? z!p>=Lhcs2$*_HA|VVd*<9#!`!+y%ZpcO04`o)>!`E~yE;LA#?P?o7pvy3EuCg%5xb zH4K#0aAVUA9>&QLT1)7^J^K;fcZ!=PQWmIbFLQ#K`djr#UiPnOr7MkybfswV{7c+e z>b;E`QGTMsj|%AKlOXapWXPx&(zjH%x1(Pr)VH({?g~QJi8^}|vriLW5Tn6QVxn6O zIO`Tco1p}$yCi~@DwdXsJioc77u-@QM;YO%=$?Ep<(29_=hg&;Ba>Huvwa|a7IqBC z3klGnTUTR6aK4chHG+ZWC$Pyh&Sw&!OREn1Ca8lZoLX#Z2-QmpXo)CAk*JcDowM$A zZPVvaQoV1d&fk^cotl`wem!M;LWnior=omPyH*WvY zl)`P;9ta5k@lg4pW}k6S?uIVN2T2)@EvrcW!`+eLJY4RoT0qI3xi6dElhkF&3xgqL ztyVyrSh20KSb6SZ*ER|0W~ZQA>WmePK?{xdMt4Cv<1Yv*r2v~LvWFb(mBaJe*Km3` zd(nX?POY$6mxtgN0XPM8{E=Hug+B0G9ctU}2u2GcI9UFD4h@(C9rTK-Q{8sg-}jv1 z7=BhQKpN#n%DHs>(o{YN_*xSV@~J={NLt*pK2rsXV7UANeuCA1SAkSl5^=u%$p$k5 z3^H+m9whq-NHwuo8BWwQ8RI!1ek`6qG|W=v*^(GaXWi6pd@nxz0^3qBsWC7UFdZ5f zbvhQ1U1N?`a;2clyNw2a15BC zJ=e{_6{PB~IbEJ;so~lAVJQ7#wOY;UVy4Q~Bd+*{Hd8F*%PLH-=pl#X*2M=v#wmMv z!Y#k;E9B8)U`sj>4`lyfs*ZGZnrV2%Nrmv)p+a6L40+AjH{0engt6$)TK5osd0I8vSl)}`BY zC`yZ`qx00>WS~A!v&i=K!;%t*)#B={2{If6!S94;V=w+Y5((#4o2;cc$gDMm08~zM zzx@j^X-9lVyMa7XlC;|Pk(l>9=>KK`xDRq`zHVW$It9ORQ*l?ETLS&fF(Z$-cpw8?AXwf<(kQLxJ+t9eXfiY>AI5KB3@wbNhd+ss10UhJTq?Ox`%^s z*ViY@R62HO_t}P;;Kal-Y%TP8>{1ZNnbf%P!)jHL^7%?>aWN)M)Bo_EwdBFl>_JS$ z>7g$-|q7>`61j}p_Z7EzIHwP)1(9WB5Uo<@l zr0hY0DET{7JC~5yn{I&q(f$%dTb>-HV!4b5-62WaTXK*f5BxcjF-c`K{f(@MP2Q^0 zWalAn1%I#ES3@l!byyegBfmg1cCsPFObv$e$v6&kPHDUdHD$Zk^q$=d2csu z`ADK7Ue)NoTeV$>1_XjDY!M$CU7w;Zaix!Gf%4DTugUE9((hk!Skl}tuDg1qShqts zDO(&zpfXuI&IEb-P=02H>d4?TluBQ)OFLUmeyHzKT-06P_c?d^HQE=0$*VaqTkTwPbPoPHtC9cX1?W&;dr7us9O`WQIF|ukk*&j`o0X> zluv^v&`X>r!cbiFU1_yG+`g`k%nfinlp;(T6P-vf+rAsg0hs{j>wGb&w#~&N&&Pvr zhN5SXbRd>+WnmW&2CD{qAW%Imy*u`cl1E!;YLWnlaK`jU(2AqIsfzE5I2an5~I!#wpa)U+@?X z*d7smfLaK08aiB0b{HG({vH3bjL+X61-R547xGp^V74!qGlf*ydYfH}TvKt6S@u|) zkGtsPh7K`s=ToFi{4%Jd{b{5GdjTE}11Ynhby2WDn<4C@c8>`>lNC(L-MWKK3sMu- z);$GEbwxE<4pGV2s4l?QwH|*Z8gC$vX9>^tYid0f~tTzXxS~*dCixM8%S8!NJ^YBPA%yYR@g)m<+eeZq(E9a?2#-TaEUu!XGFlVM6xp+iz|EemDRTkt@(^XuOXNq z4w0VE3iVI&jIS`e{=?bW$&soFh#o@ZR|hV;=r3G&kZ@$3WY-)0PXPFYcW;SFU+_E) z24)Zk{b?2<)`PvhL#!;zS3isLVylvWf7`z2XVQCFP61_tK)6VzNDhL?J0G*MnCgaU zJ%tZLr?B#hsTV;d%lRD&2ZZ|nozOP?YU6CaI&;|RDkbD6FYLPQ5znO{)sE!-)0e-b zhkxt@)4Hwp=AhTjt>qlM@P{mL#u<4b#J0`!c-`P9?-ah`T8R?M&T=dQO@%!o)TeyV zC}R?%lhVXY1`a$%4Z4TVPJ{W&d+KD~tu9)?8sr&|NqM^PkKBcS67&^N1QIhs@;Inz zK@o3w?~-&K#}1C^G1+s6bo6r~OFu=Bkr>HOd-cUkpLBWqshU^m+I_myq{!CdJU#sm;k;b22^dL1B<)B|sdb7`~^Nwle z+SLAYoi~f9Xz4S&Ld8tPEL)BB%wVAGx5zrgA_2s*t)x=9ACNCvz{%uhL$=?4eA%hECCjEgHfnE6m<`^#niG|JainpU|Mcn22` z`es^RLX4CjnEsJ(*~U=TYClnP3A6KxM8~x($(jc25J}pZ=zS!DU~_qZ)|dc~J_@&K$sf)gZAgz|>c z%7_Q3zS;r0awVY5QNZg2iNMNxpr{lHiaeTB%=wOse{}G+-qx}~gz7a<&VyH4i#S9E zc3XH^AY-0?S47Ec;1+Q#Tu3W?iu~Z})j1r)t{CuDBX<^swHWFB6696GX>fq|x@Hlh z2*YQ{ik#AX8{Ql)qNjchf0{ zImsbYJTQDXY0e1s$P3WybGxA&w}>Bc$F_9I#F;zdiMnqET#pZHgk{pVAT2W z^2@Po76>ap`sT`^Wqg@*VJ~sgyOTm*8785F=H>g#8?mkFa^hjhy77+x08hC+$bF6- ziM0riQq`Bv%H1^(!89SPJYw#Dt1^uUr-O(wyFbx1 zKp$6N6vWHHS0guQgtN~5o%92#BU^F{Tb z0OIaxeZ?B_0u4P>|{B7F$T7A71oN6DeVurgKijc*v#R+J~CPW6zxa2sO+iFYiM0JnB zfHnWrJ|$aWVx@%sWm&XsFzv}sy{sKH2TC=QnZAL_x%!ywkM^HAvusZO{*i#UT_5pZ z6q|Bqn6@-F5J7I?unW4sE7hvOykkQto0IsqEefS49{Qdxg`J!>g)Fu*fc8P)o*N(O zvtQ{LL2tKPqrVsxi^H1hI9F7vTffQeRxXs*jumNG%>P+1zrF{M(Za3?*&t~5S?}7{ zu_uIF;V>i%MYPB&?EfE{!t=i)7vVs2f{c4H*Y*6qo02Ke5^%2;=LG}U(#K?2}A?=E)#(h$SMAZbGBIv179F1vl}P9Q5)Mm_cV-+!jq zqd1)D4AX>u%^|T`yb8D3SOFc3w>ue_{r9umq@CZuhaXjuz4IMaz+3bgiU%dzXz|f?7@pO797a0uzG_hy z0uAsVAsxv=CNPdZf_zHqn>iC7m#P*`wC(aMwxNNdrl5~=umPEo&BA)pXu#xZlHb~C z1zyW?#{gr-{1THKF{vF?kmnYS7NS&mjq#A(f_u*Im5D#IpcPo)f>_aHfCS+!9e4`c`{lVFE-_-J1QeP!%cU>2;@jn?u6TKI&*y6e;Qh8+XaO~`^I1(TZY$sDBuigMh|YR)7; zb@5@qp7gj+3fXziUPIBkWR}{`&0oT!f!wO1^JfANpJzzXb_O2@m{nI z1aLxMU3>x(!nUaVnv<~1oCV!oWi5e~z)@R_!CCmvP!+U~DR=-+CzKdB?Fl)3rOb&B zSI`LcW)i(T$4iE#ssvRy)LR|C9Pms=xo&34WsbixWbxMAQQ%{2T#RMoNgEcFDWRc9 z{yB=I0QW#L84-+Kh0XsqkO$2M7A`&nOk63$YS|RvKR6NLV<;Nj6b{n# z$ZPd5>K>=5ij`hX9=sYyXizWqxJETy{b(rGstZnjz=e_oeXaFseq2V{_9%bL`8n{C zhA`9CE69lHdlNuIL#IesDI@WyVu z|54wX8GzriP;1GT_*aBT9!NW><6}gCa4wTjR1S_&3%KU@YMa9W;oD*p&wfQf*gBPg z#XI+?3-Vxl+U`yQ*ccT=JRyEtA&^OU3HL1M>eKoFM<(_a%D$X zw5n+f9P9m29ySyrdU3xMZ9cZPeZ-e8@7({d(O_i&y(sXc@w{H3k#?mC)ri78Av0l# zmJLaZOM{OSL5QSoYJ}wKi3^0ZXO?-vhd1|=YxT$E$u3n2H$)Y-9V#cP_<@tm0r-eq zUt;YD&d%!Dqa+y26`ea^05m}UdJ$BbLKSeX3Ae)!HZgmV!pMHYJ-*ndz79_}AtTk$SP2DI)d3L}w`^iVC#x@ayNNh((woEC0?x3XnfveB7Br zk#=KN8X?FhG8JN425e%XzM(Z0rpw_=EC<2X;a)+%L6v5z5*PG9%lb$pt^euQ?MaEE z*56;!GOsB$$O+OgnTu>q7fIrm^b`!$;)i42J;Ca}(ZFsJoq!~5sY4&c0IPtLqO^nI zc*?0UD1(l^M}H+z)O9|#7IXh=LdZ1`GUCkk#@#;R^XW!>-@EbxWyALW$#qv5_VE18 zB5&Q`TzJ~|C)0-5f8&?JX|y7058HDz5DrV)|LYz>kjTKYb2-*~ld6!u?x7Vg0G6x- zYD}^E)bGDiqW(9Hv$cJe_g%S~z}7A|#YUglplH8%QtDV2OUa9&+G}Etq!|!i+}6kLj3OJTYaqDeqdV{@JaK+(U137l)M!S9I=WgSNI{%>xdR0VS%hr$77lp|W9xwqaKTq~ znLy{_b2Gogg#qX2b|3`Z<-kuYd8IPVb3Xh9Jk+tS+GZHL4?6FG65I3ib=Spo)dF-iBmS?t0pM6PG^@wE6zms88_g!I1&Qbs$L#^bYY1Fq$!oEJ3(=tL0r zMGyxDDW3ORH}j4rx>rLsaQQnYdL$SJ*$}H++TZ)EPv0*q$6vjkU&2qPun0(ez&C#Z zrlUM?Z$uuUoML4O9?V_ir1wB;xy?P&Nw z`*XR0e8x&y)5+$>)BEqj9%wXbq3%8r)0=Dgb>9>+{2b3}i)ymzPsczdr!LE2rQ>hu zf~zfCmb3IhWIK8Uy}mS&fThp{D<4AI2V;mIta(Qm`0) zd!BLvbDi3rzl(aMMzk5QokK|JqlO{@h>{BI$UhcEil%Rz7kh;(kue)^BUs1Hk|=M!~fx;Htv7IBcNG0^jj1IP_o zmp7g2kEL7Q=tM=8|B(G?j{R0Q@acigZAuAnuUYP&+2*5a@^;Ifc5M$#HdAu^O9B{X zwGoWcOVqfG)M;t>fH}O^9N0oFN+wPh%=`kh<^Vp zr%IDUM6BG3p-m=M)5ZoPE**{E)MNsG85<;iw@)kvPUHprkgQ%7&COQ|hh-w@GVJcdsTn{$L{C zF+zXMHqj&r3gp0&1nXlsL62n5GKtY90cWcqk#}(rjh-i|8{%N`>V}H^b4jBuh-$VE zkSqZO>Ueyt3IqJzce>W*PWT3MkH)<~MqdIyxY@)SzNIpYj!J=%@7;x9th4X3flcG0 zP)pFsmY>y;zzYAMa&o5-@xhMuetJ}L-(?9%{OZKK{slDt^qD>l{4;)?w0b0gPl|fd zmZU;~JDdQs+NK0%RxNAtZjey2a)gFiKgAc8IVRjgOeXPG0ya&hrYh>LLisWchTei{ zv#^V{aLzd5k)pQTMEdUVeA|i;{svq_TouF@jo^EWpB~-XIsctQVZ5D5O&~R0d57Su zw=(J&+g}=A8gJKv5DS46dU&sILN!j(RY@+mF}xn_=YX%Rsv-;qPSEXu0NX+&A3OVL zn~{(*m#fxVx@MSz8c1To%KLoL$URb!-YL@iOH--xJoFalHs`z6e#C_UEPrv9=QmMw zl8gY6FLEZX5Wv~2AXQ5tP(uU;57m!~aS^ph%a%j8qGi5^y}jz2D=+jUqWF&;-a@Kmr_|vINetxO1 zuXUGl2@22>xB+m_i$s#Zv0;-YFKB_@$=ix1mD|e5ZP}hEu3Tj^#PTmckRdkRbb?~` zeW$Db5JF}-Fw)0%$B=oubA((Nq*sUJPZ^*?#2uzp)Exukv2mRs0ul;OkVas9q%cRQ z4hqB7j*kQ!%pV|KNv4Rx3&1X&+%CX(c+Ss#^_Ck~Wn9!r8on+wO1zcrsyx0+0ePswugqnXmbYKkOiw~fgdG9c(GIJG# zOkc8`%{(I;nvKE?Yk?>@Fh3-#TK?r@GU6y^BV&t9f3FH>yd#mxg&af^Z0v<_&`QaH)!xz7Cao z4MM+zy}f^L%dw~2F^XLG)TA zLKgLU)^AFHGejEN)Yb7GK0*UgX?|A$I{|BCNW7oo_OxBtRemq?t79j~ZvJuVvs`3! z>CR(qU|9Dfl$OTUCCXcG`?BOfiVpmzy(lWX%IgsokktVY!7));SWEjbD)WA1>hCcb@K8@80* z`8@ztOeTNE%pvMh#Qp~M?v5*zKCVd4-C=JNE0^SZG-(&+{5a_HAC2(kINaO~KtUud zlT2i&QsX^LAepG74D^cy;Md{q$vzuzYbRtMi%~~ODO%4pY7kQR=Y9|cIJI{5bAf44 zv@9jPg5fQ7FrQ(l^RFW-cghL<o;ekl>x_qlvsc!1hC2~Vt0)y5@vI2t7f8FJe@ zTnXG@Vi7*6glYL76s4UIT; z-~;(HK+PDfrdr>YlHWoMu7HO6fim^iRU5MIesmTf_>qWI%sGGs`)Cz6#SG<(> zdjLSQ4HmQ(pGjPG9h#!5@aM<6;2-J%<_50zKK< zbqs%w5`@$|6Fxs_50?ff=iljHvuV;`!-uR>S)V9`{cG1^DwB99V*dNj(r%=hHmedIm^0P>b+#55H1e0 zABw@FP}-lE@K+8NDOb2-Bg%4bTe!_3oU;v22Jl+yZL2!97#lu{JgA8UJ#D$+T?DPA zq!b zgWsfGZTvXD2T0J_X6x7kU-SNfec6sZt;)-Qx!VF~#`H#ovvr05^SMgnmn7g-$| z8d)8FRnLu)2Tg6F?htE(VsBBF;X+PKrk68|gy?FeXw8}M*XUblp4Oijk5Pr)wN={x zz;rInaa(h{t6B~3>FXE@#IzLkv!VZ(sa{CM=Dn6fXHns*+nzWV;9Zl8q{KJqEd(w4 zs618JSmib$;F)S~tnz!#|I4e61;Bxh68-?;A-Z>IVOJOS3d|rpiUtHN39dmIg*6Ul zT%XJ|b?VV|loN3^9`qYY7KNm}F$a5$ZLwaJ&2@x>n#Px+32Y5i84Cq&>X14s6m#b# zn0&uM6{7cfxdvz9ewe^ZSP4Lq(}PY1{uFepBgP1Z>F4A@f94J?M6Akl3b+4$MFfZi z@PRp_uw)=6d<+ORsp<_vUb()(P*q$XXOp?VSl(J%XEGC`2XGJ=(@j~2w^a5srF*z* z#RhgW`IKO)8@+cqeXm&b?B3ZupdRBu^B8U|ii4DyKsX@7z%)E~_mDX3bCZ_cl7B=o zNJ#pHda_Q4qH#T-mvgwN?D?2etM%@>q}`pRZ?o$ZBA>7+EIo&RUu0b6_Gt`d^OP|_ z;;BG*-Rd5pAQcL3FflSN-;HYTe8uUf8jF0l_}yhipxG}`$zi;9jB99F58J(`p;uRc zp)iCBNvEww+iV^h_Ple&Z}o@yzIg$nkK@s+029oPh`Sa8oFu9Lhct(V2c;i5f#?Jl z>YfgolE3Bw|Hg91OEh_&fa8*q6JZqe7L-zsEDhMrCLVz5%GMVbjbw%%WxDOo<}J)sgMHwp*t)m=wuinLP`aJPGq zjL8a)$SHgt*es`e7U8WHvwT6IQsV(y^WbiiC&ZbH#<51KAf|JU?3kOqh@b$-YrZ-q z&vbXUVY8bdO`FLcmqrPmN{_zVrSc)MP&uD|feT6n9)`-+b15yne6_C2SlbyqGT)(v zhy+a?%anN0^T$0Aa1oHFzOB(`BZ(rEexa3@*M|(z;H6( z%N*#3O*zn{R=3VJ zq`EV;_v?a2457Wd>*i zf*^;L`oRqXzp=gI?4r}`dox{DdEuBm$p}s$+J4v^ER=2!@~i(VE(1*u>d5*DsGK-4 z+r`ot!mVFnU~LaQk}Ek{wJ^k=hf_aYz}`EU2oy28`Cflj6bS1%3+55urjXioSnA^DL+QotlG&mMN)qPTt|-#N6OL1`0AoAw*N&^ z7XB){$UOBqWU0>H+rWQd<2wCOP<$GxtTaJOI4=xX1i;IANXOIo_May`O zw-iKsZh9WuwhgOYGB)v=ed4r`DOlMdpPU%cZS>FGSXh6=U5ei+Vw1x2vF+!dRuX`6 zm8s)5EpvS$(;m$O;%|y1mw^s29I~5d{c2FYXT~e({BkAXr~^j7I<}=L%T3HS&&_wy z1PS+wM;Qt$?6r=_ye)s#_2vtBIMutTm$5B3hi#YxeIk_BYka;9k~HzalPv$Jsc#pH z;)JNYGlgVDW+ zbX!x7c+(Y5)O^cZ24d$t)9q#)>zOzWA#1hwPUeaLoIqRh{64d>?sqcq3Ait%I<(YD z%OGmZCHdLGjpOxB&h4BnxC&nTy1imfX|+(F*oeU?04E8!<%H=6kWAUsgB@<^Wpeyi z@+27jwjBW$b^l6SUN4xy+b;_WLZ2!Raz(^mLY#jPMtH6WN!@)p(j_8Ue5NDw8o+Rn zq84rlm|Q)n`wx2meAo%7b^dAmf+gU2xxs@v;sQm7nK@Z>Y`%$iZPzZ{_Wna6B}tFN zrpTx>Xr@l`kWj^2Y8K^h%7{LR*6`0chP1!ltcVu<@20e${EC3eexl%{YLq zQt8>IC1~7?<|YGV#X|LV;e?m~8kt#nU6AKvxiDk*oBM5|VP6;VU7)mscYks@U_YUS zNZ_J%pQ^5Q#lbXp#a)b)KZ`MP{GOumqC`F)XqhENhooP25-+?BN_+C$Etbv@K1={` zhZtE(y81QG*+s1K`vG$BD70E3R>((UDdWk*dVj4`iJMqt6I5D;y{LiF2&~tDhn~G& zXRA;xt0Hj#^q%hPEO@n3EbLr|tAka7*(PL$*t+YTo+<*`S_2F7Oq!bCS<$#mxS0y? zS1NmYbzazwFtrWLIuqF_72S?h7R`Jb8r(hHR6E2;ckx1aUs_YMazvv+mG4Dzd~$ai`d!w zX9^*Z4FgO3PRCwK)Bu$rtr0+5{;Kxj18k;!2J84%%IbW33wOA&3SMv*Qi=6Q%y0>4 zKo`5**)NfQ*c<;=CV#EKJxR!_!tTe;j>i=l+7(tS6(V~Bu(!P`WU2WXv*5sebe!Oy zsvV5r#ym@(d?H=F8wF$nWD+QfEYUk0i8r_5^jKnj9y7$NJT3swY<3p#c>FrNDF2U+N+4ORg+>I*de+4TQDBndF~OLPkI5 zRG_Ga@_xT>fOY^NAA*OSeJ}%PlH9zx(Npqm&cM!c6_^zJ`h9n20^J=~A(O#tjO~=Y zMulI=(X#WK-SXLGA}rIqzhUY2#7gMw&Qd>kv4J2sl-5+OU<#>-uKi%DUWDA8)#KOA zgdLZki<)lBFu|X8%{c#=0yI!2w!QArrWYa1Wm?tcUp7UX+W1a;em+nmxUXpaDSTY6 zP!GB%_mPwfIHk;=MmwVK1=6$?CA1uia7!}DWT78hgfGivW8N7cG5`uGjy3^BoZOuO zx?Wi6B~wj7!>{5As)u4}D)g4x{&p^T7&mm~mrwtQi$K}Uv>e1LsN*FZitoL9?pN51VXnH)*3I=Ft5q(?ADv>BeiUa^-zz~@-|uOt1yK)K;0e+rw|29V3S$vW({k(%$7 zNo7;L1ij5=0d;`#kUb)*leIzayu_?rSAa!}%$SQhC2gle60En~BYL~f99eYFxp+!e z$ToI2aqW*_dJ<^+fY@yMGDGCjstQ|O|566>LblRfB7c^%0AbeA=r*7m;IigF=n1-C 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� znse`~P4Ko7$dyijsPLg%^HV6^2}(cq_(IeLH2B2bsnHcd&@B9Sm>Z~Z``q1sL@d8g zS965}n(sU@FxXxXzk;-YTBEBooqg~Ay4{@cHsGsW_iv_v`UyJ-q+Ze<42%Xqv&K5d z2IeM=KZ&_TrLPF}MdZC26?@Oqkp`|yKl06sI`_}p7(%^bQ&$Nyr>oAh=9gLo*&Gw;;XsG;&S%3a1Z7U9=|3eTp@OO{T9zbr-5E4CNy{Cw;SC!qCBzBn}kx7#w z3A6p7C>>a8(l^^HZodlp3b*WBG*LRCf(*w}!4ARa%Z>)#ZNK1?L!U_@3|*89Yes<+ zxZq%OBO(oB&TDz=at9&w0_l&J3ao$Q7itdX5l@I{sJ1ixz1$i95o`l-5tP(ng{q6gzbxQBK(M2VrX8QYQ zYNaQ2w>bQLN8R`>{u+rxGN$5rS#w*HW&sJwX=mbH0OlqV*~_@$O0r@=(}@vf(`?9X z2P{tP{R8<7^=YRI;$u!H%2yMh$-AfkHtQ})gRk|szf*c2q0I|Bwzn=i_05Q8@ z=%ABgU^uOX$F}#creGz(M&GYQdL25*5+2~c^TJh#5}|caxq(VofYS5lcDkLZxgyFu zRID)a>tD~3uz_$fQcDh$d?czL=9n9=Fwi{|kY&7&>UZPj z`1gRpn2L7=7o*cRCQb;MKSX_kB|s3RCwSRm!`#>rKMZp;y&7I8y`SzEKAD` z%ICnji7~&83qz@sIXay;dvWVvjYUD;rSL4b1LfV*A%7eXs(@_Mzuv@XH*M4*O(C%p zT`)n(6OC5`f?1=bI}4P_!t&vWkILm~?~W6bhkucB9&9rY-Dec*RHgblJjoKUS)w2n zZl#ilMbGQt%JDqliIn#1-t@CAP;2RvT=z5}c10V#WWAde2nim3qR*q{6IHeh5p)4uvzAf-K~6 z70DJif(w8gY72VTpbU;P3YQrqUvvF|M%n;+C|Co6_Xu4`QT73S(JZk5(MoLPG?~c~ zgJp6&65$d(AS7O zVvkN~Cp!GX{u3_ChksNU2?e1g^6K)=+p%>*BuX|5?7)`^=t$S3!4p&*jPJS#oJPy;jT2(8y>b$7`=3Z)J&D;QrOic{f_i17Z3NTza%7u(? z^tyL|o?cJ!#ShjG*?43A#7~3{WaR+2_^m_AaV&i9Ln1^EocVQ_v=wvQ3*;%-~E)~4&m#(+GGY9C1jaw35!3lPN6AU#82DSJ{4Bc3aD}`%N&dZZ$ zOj@P$0O|ne4lXD=zb!uQeJ>}z>y)~D2q-|)GEbf(p>?+Gy@FJSRyh5xUu>e`$Ty=xrOO%$1>8SzWB|py_oU>#6>qkccbR}$zLw&LK;fA!PmrvFinl;0o@Xc6+)&fG zUlocgg78q%!Q%gD=rdi2P-@Xn!U*5pTBp>J!@~eW?b|@q7*Qf7B#zC)!@TEOLl_PX z$e}u-4oHKjKTmjyto#QdTZ5{cm@%oP#btT$nyfEvUW(zi@z!?} zZcnuZx6GMT<%w1BUM)#mWY_|01%tMFCY$ zJyu-fS>`kkpDjLL+7saTBc=ih<2Qr)hlI?cpt-`X(P3}3fzR1UAVDM*z&SRM7?3Fe zF3!5RP%b@#=YqB_)h=@(_7T+mqt=__^}ivy&2jv0_Bk@{ACIH19*-^YvAz)vZ6qV^ z3GmCzEa;C>v`t~(%{*V@b!{75DE}B}dLOh>_CcRfmsefmz(R%q5u&ud2ChKnQ?>*6 zwFossy_K5~BB2ii{l%#*j`(Udcp!3EL7-{Z22Nz*h0-@{;Wrp@Tpbwa%b$B06io|I z*~xFkt?XF1Gv&;fM*hU>ZYPM4s9BdeNP4!byqw!l5NCn_UO)h;coW#@9E`}3^X6ZsqG^4zLn*zs#S(3duySUNBS2Wm8e)ik9??Y#aT1oWVB0XCNb_SwB)1q#x zWQlMa6-ppZygBhD10fn+kR!mzI#jBDjfND_rq{4@ygwjTY7r<)4rpx%;o6@2waA8r zAlKW@9HMh~iBpOLZC1$<)Bvh7ECd`B%N&b)Me11>JO8Sgt>crLqVIM;h#h!WHJ~Mw z#@;v|#sVgsuLn|2k;;OB8bA&H6PwrBcy$H~+sIp49N|rzx=+@Q6GAWA62k-cJ%W|^ z7p1qU<6B>ErxMG&D5MuLS14-J08=UIq%YR)Dc@KQ(EW5PX9?zUSUvbl`dFRU$UMT) zDW%(0U%qVTy4%I!*bhR(5Ja7B0ISzX<(vSkCjxHzUYFII-JA>Ca(;d&!m~%Z)kGC8 zAoEBfTxD-<_@chJO{AKQt)Iz*c}tSP;5zIYKlpu(20LYRUWk3zep0nrc7m3><>-g& z&*Qy6WIp}lu)e3!e@EV1{hSXi9Wl0C_W`kva^i#vP!~=_HyLRQPs9fM*n4>+ zT^a#O<<=0QU;m_x#;TiTAG4dl7-ABTfJ)EBrW$| zCQ&C!75*4+2t1yd?y@N0f4v{l(&-Uh#(N^2y!${U1lSh}8{1fwQ-3d7(L!8S2&JSR zO=%e#5(1V~(X%Y|IzTs?RE};vmrGX+r+7{ew)^ZF*sj}~Yu~_NDu{xDN3IZz=PNyo&X&O?ik2rYiWomMxeUO6KN(NkB880xX zN*+N_M@0+;gJYgMYPxZ7vn7quvP0S5hk}M^jHS%o8+)FdN*in|gbBSLUM3v=2oj<} zodN9JGj8K>A}&_snXK*Psmamwb|I(^Qb?<%2_1HC9Dt|^|D=uAjMdpl%YL$pw&0dh z5-D10io_6WBG>?8_UG)n2wrR-qKc@o8wp~Zv1MLZfV^B>fbf4h3nKyN7?07}K=+*9 zCvTA)zrC_OM3+CUK}!x_-Pvuy<$K? z>h|HQx_p_wgC{k8b;42iQ+bH49Ci!H9V3O90&>^-(|)jP|2D!}WQGBoP%d+q3lWID zs!&Cs?TS_E7fIQw9`#q|+Y^lC1R_V=2O@tsrlc3$5QH82f0c<#_p@Z7X_)q8;e=cyvx z*-o9Ko#Q&3JbIYQCnmnfO57vN1SNO)6zEKyCoe&15Y%RZ&j^(syVi1mw(xR9Oa*_9 zhgJOw4e_z)?t_UStWQzgp(G|VoPdZu61QGyFzof_A9H$oIRVN^^xUPyxH|AfcO8g; z413%aY+&G)TI!Yx7CuKQ6GI9XRzpRv^3P!C+>n?Br&L;B6@g!@z9Tex}ra; z%(O0~B}Z&6Kmxb}ELB*G2y##)FjG-sV?a}XGO=1_l|^<-k=&t$f=uEDet4%ap+!|Z z`fF~|+Zr1SPPX7$qPiY{2=a`>-jRsZA94eQSE<>9sdbn%OWQAaOz7>|;&+r?OhdsJ z-V!lqrz`}%E4*kMYO&*Yx{WN#e|PYws_;n_!`@h3e$khR`8c?VIS0a3G24hi`dSNw zZ&8r+w$g02Jn%S;UjLD?=pCFoNlmFt=ap?{5j<-duW~q)#-#a(Ecg zXP}TQAk4rr*+W4aF9P%-FAQEfDRPb$ye2eniAlj*J$&sT`7k00BQl;#*q#mj<;Ygp z-{IbFq$aGoew^>LMgnE7am_X$PId}v2NKI_>t$o>FK_%jv8A<9_;hI=ohbP9P5f}h z@D%#<>3swD+TU$NQ|{UGym1QT!bC?l%nzioU+;5_obcwb<{D~pMmE-^@{{mZT5U!;-#CVs z0vMueLgfOeLG0={x_M%|>x_qo9+zuI=P7Ar`wMA#DXGC*?V+og)s2EyFT9s}0ypZ< zj#*e1R9O}lm{t$5_b^DD=si9nfoCLgx5ysm9`hVnL+lG7>bSU+XD%IK!!o^C2y3Ac z+x!z6=oE8F^5*=}Q`yGsl)ySv)-PYfc^Y*7b0Pzbumh+3Vbt#vFoZOH?3_svfM;ja@FADv&So4y#Rm${cs^wGvWbkty*z_uRH zTau&`sbk_2$Xigmj1e&bPFVp<4;MTKi`aLI=PuQ$hAyb;8=S+hD2|VBtB2{v}|$yt=Fm$8zAP(c@v3q!^@Q?rM&}HofjkxvTUj^cqMe6X30sW z?qen0t79$LQGFor_+^L+YDB8=aM#;|#Np8?SYh}6%y6erz1Bu}Vc3e0Imlq2@GyS( zFCPKL$fRe`V-aL5jKEKlER%-j{`r4v zE=I{kta=0>vfR8}YwIR4m%Hvd{~lq{|7lWuIjs+ueOYdB=5#JD zNI7F`eDQQ>hQ0XwgkSMU#pqJZCQfPM`AyTbKA~?KSECE6MwiY%e&N6JY+q<>Uw_W* zXT^{4bat=9(ygnnPORVumq;EHj2rAz9)}#Q*xnE3Pd!Q*`suUqzN=bK!NOwthr*zMJ zH}3sB&;A3xAK$~n1CZ;!=8Cn>b*^)*+aT2!vV^xOZ-YP}LU}ojzZp0@vq=R4!9ns;;;-LhZKvRRJ^D4;T$LPbqFHLv-EeAaP0%(z zFTptzuPcfFl!gY6CM$sSWfML9vLcLz{#;HDIpA50qvY^oKG<_lz0!JS$7j#l_o=qv zOv=fLt10?asK>(1b+rMLG?T)&5+<_nS%;h&3I~CSkst^Rgaw5+6M(}hEHfDX`RU(Z zMA9R%;UfPtuOe}6#)a{1o@ zH)C-~nz1-K#`A9f_sK_~&Hp|FA)rNK>qnNT-TS|lg24Qsk^epeeINzGZH5_Tvix`P z_MklM|J@z%!5o1gBBXemjQW4p2`p0;_wV-qU70>H)+3N#XxO*^KG}?wPV|3!1C9y? ztN@4Np*qcfHVqsU)&D!F|94RT&!Rv8Lj3>bdV#2a95X{cy}2ThTs!;ML2=4em7LGi zFaI}cKT_?K&=oOFF=C>aNq^4nlBew@@7BtwFEKTz>1*1Z#Z`NAAIv&W5B>H(ROGi! zQk~{oouK8=+B!>nvxjT5enM`SQ~8A=dQF!75ma>U^t8Ype3i z>hy}WX(^SomX`ho{`~RgK~JoOr2;^Z40w9Rh^3xEm6bt-1zJOA&@tQn-$7#ENA;?HX_fubs~Oa*P-BEqB`4GUg~CWe;7F|V z90(yWeHfNSkwTg7rx=ww25%hL^e{mO?`Od_aH4-3_k zL@#L&Z%Q`j;V-TTfsLmd0O(1`O?&I_Oq$54lI+p!q1|^Dd)HMq`1$bFzQay3RPamf zMr@(qEIwOAh+i^@4MD3;LB!r$Mv{HhO?*I&07mxmaDfe&1cEYlYEmz)X*y*M^GO(5 z>eA8?9b{B}W7u7#*Y5ptcYK9io4pS{#Gp~z&Z<#oGN|>#3LkK3WTshN79YQX3gtwd{x2M> z-P?#0M?~LkyTQa|TXU})Q(aeXPPND9ynDpUIVs+J7d+Iw_s&8-q{&WJmh$>QyJaXT zL)ZgH3hpYD+VoNwB{H{y{%C`>qR4x5sTop8666z2Y)#-i3GnXT*SHp9V0wQvGu%5@ z9!QS(2$9!KLT=A;Q!VwS454*x*|2_nk&H}_s^``;T#2laD z@=+CL3RNQ2Av5_6y^A_VRua%)CO3y&JL*v}%~&?Z)QX<8n%1G^86I+!?>rPf>aB2Me_bTU%F?-+S!wI~~+bJTc z>y|85#kp<*c@zwZO^nA417@=4K(%O;4vH1jiImq37gjKMwmwe&NJrd(kIDdh%JH2u z;>UfLSj1M+URTjs@7M0+`BMMjg9Hr-)dT$w<>%JT`UrwJDyMrs+!}b?H?Z58aP7}Y zd0n%rj20&y*WFRkEB{UL=Z(#6F0%t)zzB(9fJ=Z3qVEBlV8Fr~Vi&pgoVsq^>LI0# zlMZd(R7MBji%^iqmvqeOQgJR2;{qHc3E&_TC$gT<{V6)mG*Vrb+f07e2$9J6$V2 zVCp}aOWY|O&ntQ?2VhqoK)&D%Ry3@T*E9oVDc-Nev=h-(T0^^Uf0=hAAiyEN8D~J} zrLSR_HaHk+t_+N}I?Q+kX<-6SkZ6<`6f;FWdNiQOT@E-p ztvCo5MrsO&LZ2w7BbYANMRw3#T$9%d+N29~vT$<3Ze(*(*_8n69|)}(6~q@=3&6ri z1y!^ptRGVQTFVGma0zE{F3n6*wH^=h#^yjTWuxJuq{_MhZ!?1qxHoOU_-a-ZcZmlg z;Q6MVu9!3N7pIj+r(0&aJi&sfTloN(Tcy3>!M0K?P67wkmom3`VNx|E5mWPi50W=c=ieiJMb7Hku5^b4K zi?s^W^X*?gujKyrRRNazXAU!haFB6yD*2H6jl2Uu$qz~}a!2>OxWMsBq#G%2ok=De z1@Pa?K03`eWOjD4hdR3TuYK$p@+-U_mnTm*(*!%bEQRzaA>3@@78}-hKoX_*lZlcjUgOpbkH90cc1n! zd>7hn;^y1HQ_`6ygO5ymAXka=0su=C(dQ??GJROzpc|EB+p)_*5`#h+*wh-;~ zs$nh1{b6N77H=((Aut2MD693SJaFylD!7-i3w`let_Gpc5QR^}9NWm`-U9*gI#={4 z=c%!9^VuYjL}`!{fhs`YHj*d7noBtH!*e?m;jT>%W3mQ6@d~Eb#jd|Fz4EKv;Yo&U zN;aF{U}zHciZb26*G~+uyu%+Z@;uGiuuI$@ybr#FcHBnsnVqY6&-d!~A)7_nu6&UK zfPM6oJWotigi~J19+jw2FA!`f=uw`na&LW!PA_;%Fw%-x$B!R-=Hx@FP zNd5~3gV5*XvifKor?#GgIl8VE1fY~=Ei8^Ui`A|MoJKW*+ z;oDYqL=9*cy2U4eefl;Rn9Lfzs;@G=d>jtHt(5GG?1C7w;74o|13Ll_O^SU(ly1!B zVv}8;8Y}>S9LbC|aqC?pzzbCB_e2iAyscmzCXl;>1(iwx(Sg*eh!4cm#lg319WyW! zvVt}V>vJ$RQebrn#N#F1_o?Tj+#15BxNAbo0XQxPZcl1TR1Oyv3sJaT_=?DhG0a{o zmTQWMf%ye+yJ0ZZm_K^x4urX*;jcEC<(6#VA!ESUrP}U7!6+ptkDaA_o!-elLk5Cz zTn=D8#wyK+*Hvq0cV3u&vOgiHAqA7r0QgNJ3B!R5F%0C?FqC;4_o*0!>SCO~L=l0B zp^?p@(zDnx4PWs!=^nvem-@oc3>DEguM884?#zB8MuJEjKfhQ0n-Pr z4-1k82r{xI@Yn=uhe{{{tqM!ma<8x|L7 zh;pIu90XFS^ER-{kmezm&OzjFF#{eL+hQy>ss~trabGAaERq{s*j*UXUb}=lxRu7; zYD@rtgM|Ks2WGFhloxh)>@N5}gR#LY3}fz@DKPh2BIYF@Ytaa4q@Y7DI)&}uIZb9k8#2f^FJ@YLvaUePBeG6h$a*w*$Q`=2Oks}NBzmB|`248zKrruun z(^K&L{w=&4QndUDu3mvD=4)u5JG3Nq|K!&~qfZYwCS7m%&=;~unArUYmluy#zvsVQ z-)1HT=6{A7O}h5fQ>f<*IsP$NK@x2t4@4=&vW(XspdAWx!?N^mFL_I(1_ zxHGgzclUbZRtrSB9VV9{2F!hJ6Z+Aeo})I+`&Wr=9XZeSp*bWN;1eUTKW%I(GT0i_ z1s^LSUpj!LuwG6h1F6-)B2yE;mF#g`O=}UapMH1;Oxp^5G&4sr(eAS-vX^G}UdUBi ziN6(v)P+328t6_nC&L0~{~F6)N{JzId6X26fsSL70NWjfT2JoIT{vV(p(5#RS{ACi^H57v|QbV16Mxs|AGuK#+uFBRKn6HhZOBjWt+FHuUvov&wZe zE=SNKTRT0s=;1mMC!Y+nhx7o;8KIv)M;{JfH8m#dw}YM&2968@_Cu^Q1xsLobR}xU zQ7xr%jC_t@;i6-@Nk7z~)>9jpx%}B-YLCZ$Sl`A81n5+Nn0de`(F`SvJZbDXj-$Zz zCJr1$d`}@J1QV;KHMcxRQ>Q9>)!$`;duwNiLv9eH85)?*h)T67+)@cUwv-*#Q@*5m z4)9GFDo!k#3Y%2h^~n=KT1l7!1d2_`UVVb2+GQPU>;_b#`wVtaDdUOD8xGn=kZfcLIIt*bk6;0nM=NNe0nV35_TDAO?e`%Y(b#lX-hCJrITkK7rofqQetZl7?)zab zx8Dh(UAUiJ03Ihu@_?^{Vs;q4kL<%2BiB!SV&;IPb=PI$y|FW6M z3wa3~hg@$!84gMM(%e7#oIVIeRd>}?zbtb7#SB@L2L;GC0CjC4BekUKlUk-%?H(1G zOL}kdGl5{H{{%%5(l93+rO_~_Xa%l}Cn;+e>S|Dz`SY-*d*-y>1Q!mUzi@ zkE(*(e^OJrRFML_vcovQtURE=VGlBMb<>&mwL)2}oap09?#Y|f?>jwwz%EtEx(Ixe zgbwb0A&pnm-v(V4Ss9k2dkeA50aJ*V(wDMZ+qE*vyx4N zvy8w^rb9^b#|ty)*3i_jI7&~c(B%$rd?d+VwsB0nu4>?YTA&e8$#cDh)oO4vo{=Q7 zD4kojRLnh4T9U}7M1u7L#OZx^RfKAfDn+<6s5RoAnE zT$Q=ebGxS7n5PQEj%T=yQnUsujFLCh9sp0kWgu+?_7~3I5hwPBeYfA>m`R1VDv+jY zzm_kfaTbKUpxC`SxvhoSNT&r91^~k~Y?7v$YI{W7VkL7)D#Sg(p&)ZRL$Z6Z92h;Z zQK}hg%$U?xCijgLP=Hq*?mfY|OEa^rC;EB3T4c+*ni#Cc1jlFIB$sGZ!UIqZvp34A zg-LLv6Zt#)HUsPn3T^MQJ@7abqDof|Kw5gCMepnFWl1WE=1&IixJPBJ{_4cg5AegB z6Px?h_ah7hRbl*{^p22t@Lhozn5rw}w$a#I71s!cNfp<#Y9NjSH)}~283um#Xz66j z9CJ0;`kAVtAIOvMGBrbyi#SqOB9O_7@4MBsGFxezz%GH){6YF)>}QmXaVsH|SKHAK z?gK>r9lU)emn%I3n#0e!U= zSu%;MNzi++MlV@8QH~BPCIsD#2iODW??zK!q4b#RBqmZJ*R_HNTp!^Y0PwWXDhr5{ zJz+0tbjpEPWhye7mo5Y82CyO*68>*ZbeX+XB6s!bD~pC7`5^@%=E7f%Sm|dX;Qf`K zl`g*=H&K)N{4D{%`|zC2ExM!2II$nN_6b`!zuX$wpw^c`;e(<^^i)6$A;mt|5?vi?$@9#yyVaDLK zP)8HNf&Rzx*2P3M#=(AY5MY{65Uu71$@Hv&+H+wbY2h!wAqv2jUy?nT9X_AAZ=}QW zDtD0$k|75KpWrv*IevKM#ov7{e~kyuj*RIhSN6w>ek@d4SwXk4#z0cPNITT1 zQGmM^@sBxZ+Ne{*!fBwQD-Lzn0NsuN*Jo&yWSENk24| zzWvuc1Hq`GLL1GZv0^&1H<|i`#&XigF?ANfoOGCz%@_LZ2+L8s`Y0?v3%ov>fk&vtN zi?6#Y?bYRII)ghOZfqHZfmXMywf$jUc}%1lluBsAO()YKP!QOl|y!> z*qz&nnsEWKVS*;N(xU@64z;9EuB!xXy-bnbJ~ zv#(8|a?(6M`=UllWv7h4V~Cz^kOXD96aRw-#3Ug+-L)sbOU`Pm`%6%syvh^zNcqp!24JX``0MMk86%t}Y@Wbi z$PeOlO>zQR$FDJP`7bV)(l?h}Cb(}8@R*xx`$K{CyQE2AFy%eJozgd{2ar5716AI# z&{D>+e)b^wEbjkB{hN4YoIh`+V$V+b)03Odn!fKN=o8yR%Xh;t6uEMto-XCqX!u|c zj#|`>qXTn)k7$-Qn$Bk$5T3&QpjQKf6L#NHA3iL2QsLtuU!cfPh#0m4U;G0Lp85;k|Go)!J8)Bvvc##0MJWqxfI z9?tqd^OEtq1|Al2(~A!LFyjXUk2YhO-xYm^PKKLkI*!_1-cEg)Mz?_bkDsQOK-vhM zfjF@>q-(_{S5z#GyseIpi!hc=cX4-IRjDH{N0%+NtN*(W54=aCbLX>~wxNvL4IlWU zII%2WwRKd_gTOW?<>;xQ3o#@<;;+epAYfX1W4$%pwdz~8@QJ9fePDy3#o#b(*dZE$ z_SZ;SJ-X%#r+fP^zPu!TFgSINo(RRxGH&g>ktedkfTw65JMep?!(;2@JB!*cQfnqM`QtBqXX$B_A*WpXU!t$fzjstU7MHY zGd^FY%BTu4^YPJS|9-v=nDk++NXt-;bc-OsMMtpwC7Gdje-&u=yIs0wt!5!xng0S6 z5^jsv@z_95#6J&S<6q#g%d^D8Yy0P$JgAg9Ws7K2q5?1VsldO-#YJns$gx%%^PbME zHMBL2Bz;!(-Vg&!J4z8$Y9Nxwy+KMx_3!8B-f${iZ{fX_WUeu8Czh~O*IxfOTr;ZR z$Q4?%gTEkhb526wnD84bpAryyzp5)8#`*h%!r~bfK|4WCr7`d!{~i+u^c_zd;5d8h z9I95Ya7LvczFJj{I+PI_`VsjP-0`nRf>1KV)uz5j!z3xiBoM;7ySU99er82Q1)R0c z!iAWg9}fQm{cPY$8^jWin=!Q)@_QhwE)fyHk9tbZGA}0d1QHaYjk=5h(dve6n)BRv zty7LrHLEB*Q;jkb%SH%0G`TQXvUZYBIeHB*BH9|y@qe~01@`B>Ty>h6-Ensafa>&S z0D5q__HOp)Fdv5HCSe6fy}JxIv;Ku~0#r2ZW&8c=6zy9v4{O$efC7Kp&8tv+a__US z=rYYv-n;4>@_KXmr~$ki5UY7IRx5nu%dAQTfY|0Z3!8kmV%9OW)okn-LZg)#kbVA6 z`~>yJgo}un^(hh0E3V<2#sir)0*Oy8@h*q%wwD0U6(69#^Y8ViLY<4sSNwJ151NGc zBi@(rgGl&P^A_dv_1;{cY{?Zu?+N+;tyclX32}XWGg;j&=ZzKL!cF&~kykUm;$4VQ zh}mngZ&HzRo%sKJexxio^HS)o%6xCCEWT8^`KFW(f z4jR<3Y_(Z?J=6TxmNUeGk}kqId@7rZatJ2fc*A?A!0u0*vN~#4>NVW)3NiJGsFr_wENYkr z%Eudzy(ZR}P8rlBmVwbyvr+L@3}hkPk*Qe#5rjZHY{mGL%6(4cXu+j0iCF*3P|!NrA_I z)*53Lg=6D1o+=csD({dhpI&KORd)@-RhBTh;CDP75EjZvUlg?-a!SB&8)zdaJt4Hh#xc9FFbIYz`;~R zSj|*kdW$@>1J4lXF9pb4ruttJ0aK z+?hudBu=ov9)7JV^+wza9IisSREfnqfW+HPnblXwc3ve#di~(hryjnDo z9N+MW8pgn=tTIVvi)SRI`O0JSD}w=Iw_3QZMemp`pjCk+@~aU3oAZ#d3&pX{-&&Qk zaqj0{FNw`{Qk}7Dx6eVhYUcCy2s{9$J(LlYNX&FfZ%}Gr`*b9nC;0%w<|_t zot;aekUi@0jU92tP&)31o|R>|E+x1-4mgR=4LBX{Dt#c>B;m(Ak!9q3k^0kD|JXV% zr8N{}+^(FRZ@jRVq8I97-s!2vkWXXv7>{jk9w@^qYp)~9$BStdBpyGFG8jAv-I{Ep zob06-6WJi`2sfoLg*!qYDC@7;VyqBBiql|GMj#&=nCq%aUJGAW9zk3ahJ6g9`R)=R zW)mgmg5q=e!v2HxJnsd7Ef;R3cXkZ!?Cc&{clTj`n<6`xq=k_LH+iTeODoeX^jUH8 zgWP|mUAC}OF1_RV*eO*+b0u0eWWap=+&`-~C zWM zux8Qp@a^h*=SB%~0)lPBh;@_h^gL_HoU(80I6%mJZ|}G`8>dM;4fSPt7mS_m`>8a# z1jtJ_#b$Ige=N}w;>peTxvibs^`7KB`(qX8mixt@`_)%T|L5P*xNFm9>O{Edxg}*S z$)Hz93ujwt90ciQUmZY4<+f@)7dt>MtUdfIJJ)-V#T#7OzcS?%z2k#NxwJ2+NS(~d zY(loE<{guYETGV@A9+xgX^sss&U#G_Vb`64bG^t13Z8JCsqE-0fYv)mu8%4B2ReC> zjhFsJIDKXk(I9P%F>RWyBvPmg9?D|ku#KE4rb~sZs&>j6fLVFZ^v=13AgtCuMlzN_ zS*w}L6H6b*`IL+Ua2ad@2Eyo78>SGRMVL*cy`Q&^PWhsH3M^bWUtL$KUel#vC}_NP;-2?QQlNy0o5nbGz1MT>_Ujen3O!_q zaR8|O+csCDUYH0BGY{v;NHjpFkG&R4nAxCX-FcqnKd=V6@_^Du%G)-%$=RKZm%e1 zzaI4XYTSk;CoNm!#HZ|--J~E;Lh=BdK$`dMMDMcI)1X$&?N)KN(n6G|B9LH4zw%=w zV6a#H460F518-Gc{Jv&xGzrL9q;s7CV0m*jV{woyK;U;;OR64AB2fZ2>`&**y^T8! zK=rRKVKWAcHyTGCZmN~)oaeNyAHd%&Zpcs!i0V2+O{XfZxFO&Dx$YCN9{Q zhHzFx0u|XIe&{_A>YEOlm6{<5{UIA4$*2hthi5zmYG7$x(>tT7?&9Jgh6r=Av$d_D zOI8m@`r=Wp4p^!1B3O*wEZ+M@jNc_0ss7WwXc!!JlePSaUsGy)-bvLzf)Rd*TEu3A zp}dW;i)C})mwn!E#EXAAvEmC9)EN#rRD?n!#cR?PQ=?$8OBSDv-LtlxC}JEMN`RcF zM}ZCz=V#(4Rg5IZVwjI`-*l*T@kv_JrpSI{m8+tvX7nA11nvCzXKNO(3urEaX_50o zYqDVrJ%`Pg(;39~2Qa1O8Wwz9*jwYDomI+CTX){JCz5mSJ|ePH=zNMb@%|@hV~>Hu&Hr?MYIds3JLgiB!I@T7(=*sYVnykfL>RuH}{>eKZ`0c(PjFkHDF)EklqTkMBp+pkr(g+Rq`Z>^t@(4 zhderL@TV2wDrL^vHjhSh(CKgh86|68gC##mAyI=j>uTwF$ymK~Yw1 z@`QxqFhQfPZ6+c#3iyKCH1TD9=5!@6!&{tD^`=$GX^hFf$(H)> zpQ`5+SgQSh4koJ&Xj79sK5}as0#k4eH(|nBzbP{s!QG%EwDW>vC4M`HzX;8Ln8TlP zU46NY^0&I$+6(&4aZm1^-wspml+*N`lgpSiHKu{u=^GL$9$FnU6Ys1M)5eCW z3?OA!I<0yu35bQ`vT$GBPRa)p2Zld+^UZ|nJ|O!p$Nwl2f}bkbXijsv(2$`P%vDevS3zn#OaBT34k-pBYg-No&ON!@ZFcm6@s3watVs9zC zo^%uWlLk5>o1WDncqu3AO3eDKl`!uPX*->)ey=HJvTjkxRrUo3L_r+M3}>E~+Ik&u zd+XAyjx(-GqI}zz2bWl*whuE(hG%hIJmc6*fd-O1N*K)PtBFb|(3JsnOh#1lqEa8u z|JpDoe)6=EHF09EC%c`A@kH4V^HSANWyGNkURQBi^C}CbpWVYv zSl+Aug2Dj@Y(bn?R5F{y{?O#TP1jEN(~To}Yzp}UW%$*ow3Qbvcx8%+BDS9rNua;N z`BjNvXDVt8@%Slx@GEpX{4%0(OXSbzc4B)LrDQ)iBP8qTaVCO^Xk%5!C3&jhsV$sw zTQ2DiSWo0vN(EcvR~BlMSEj3gCfJdzx&GGK%4o*!zaV)XP__hw(n?^uq+=nP3w6tJjnHYsD-)V5I`?QZYy))+c2PyvUz~de$A(nu&vE=;bQp!e%@QGK?N1f{59AM&SdjyJGvu(Be z&Iy-(*pW2+(QBc|wafk&$9C>88X8fHt2F{{3dwX^u(q34>DNl@CKhgA`XhyzApYB^ zE0RbJ|Cz~g_TKsHNU?`mZ@Gxz9?1t$3Lo9=1jNR^h01z~uD>$XfvgrD+4?lce1+C6 zxumQ<00hn3Sen#VN4T8}RX$WUp9B1TlO?!d1C=!m7In|lg5VS!-I6aqk{p6WdTIL~ zZ5&z3Ziy*t(RV++DdvfZ##N(JAu&Qu_&`HIUQJNpdot$tK2QrALu)W zf>)a4x(>0ix3FYDrAYENh8@;|y%g!?G!E5QGJcTR#GUIudO;Q;4~A@r@8Q9SS+EjF zjur{@3?YQu*7xrX7~Ey?x~(Xl4l{N8T5eLS70i>|#ST;&t`3T;j;K?YDvR~Ka&$$L zp#ja4Ptkxtf3>BoLJm7X0W!8c+z=un)@!9}*=?#c7)$(>fPSE{WTz>GV@q`I*oi}1yJ%d;=#A#d+&mKATgy(!Cjr@Yt6G=Tg@!lUzsxeT1 zp5f!D=UqQln&4w;kwH?}(?LYMp!AaYEtu& zH2OIrG-T@;QT=2htbrVoNIs<}GBGM~kML9a7Ycz)pB3XO3zDDJQDq@CVENeofI0bh zo^J0lyeKocEG6Jf;L+!wt9KWhi>EuD7Q`5+kD5r?nO(sWfmKrz!OIJ{T#--R6w8ez zD2aXnIucb#`eneX9yw%MJEnkHyXmd+c zK4UH|N^REH{J>@y{)Ec_XayqEL%;IsyPb%?8c(I{!U#)p3FJmB&5`EwIlS7kUSHoD zD+ux?v`Jc@z{7<}@YYz@4`;+il|nUJiPh*;e=46cppSf+qJEtNC@DyCW~}M>WWL5? z`(ApC`slF9G|bH0Es<}K>k$$B^)`*CPm1s(C#heLRy+2GWK+xUh-xWOe*tQ_ugahu z-;hG7O4C<+XR9o>PGw5e%r;Gq(b~;-1D+&&@CMNu5blTAiL#GasE-ShxK#ixy2n3T3OptzE+1=+b`g4B! zE`t}vC&ICZsM!%NGd2a+E0t8`i1e?|K@Gih-%SEsw)}#G(q3$TTWI6!zakkp+ontD zE2M?t(ryENOp%!2n=XM3eu5~3p`OerVs75sQO2uxm}M)<^d4`HxS%T#mW~x8TjCW7a24=T)sua|iFw zUs1vI*#Mr(wvUyHj#S6Ay+PG8i{^!JFA!4-yDv2%e(-}XhdX}mN!Cs*;TZZ0NAi8| z;4^dAs}&OU6bM6LhedP|=SjnTKg^z&yG=>%+X%VCMreOh*D(J1wKPr+`yD#mCHDCZ z3+c*7cuz@@P|(8n>1KxC*U%CAtGcw))C;64@={#wFxLoZ#D6nvsQ6~2v#E1V`i;XE zox5ehw}CGFBBRc?q)bhoJ{>-)T)Ru|Zx)*{W37Ouf_NKNQ77Z0agb>lr>AaXQ(XD& zoA>$K#v!HUOinD#TRaGpD^If;Z^3~(*(E8N*@?n+MS`=}D@FwHm5FCSb%Ccji!M3* zDFuFaE}qg8?VRgrbXsfur=!EK1~wb_%{5KcKTi~P$6zX-R2X|kt||k%h>a%uNI}pi zf^3p8vo%|U)29lH%Tz@ERqs@xi}tmPcHy!1b%l08O5M+BKSxIy`1bpsdSq`^{-}tA z&gLbnF)mF)!YQ_eg+^npzgbbQ7OUcoli5z(vqarJ(F5Ph9@8rddCp9*r-Po0K5r6- z%qHz!FAjciUO5PYp-LIwT;8tp_8%?sze>6q%yBRML~Tme#HnGN4o{58=RIfT>-w!# zv({ewl-%yPO||!Tg%R$!p3vy0!7ML4c#RB+Ms-`$u8PCw%$33QP(Ut1|1F0d%FH2s z87BL^i``3Se}i;JRn@dkbhM^Yv?p}5v=8ilk5v-*=hJwIyi_>ywf*&`inpSCoa!{L zxaxDk;i*qt(cxPgg+9?mG6!(6_aeIn8Ro}CN#Cam0!oJx5*0O|4S=3c%=^H5R@I#w zMh7|s$GSxAL#0t*$6dF}$^|D^IXBulx@j#x?lGRXLFsJ(?Bj20n>>eA+KmS_$abWQTsddrv7zgTrG~Z6P&MsiN$_=;9 zBI?Ui^hlPmwdEkCRo`tD@XKDEg>Iq>uMC;_WVOERB`T)YQARa7r0@c9Ca{ z4kGyYK+;{t-{?27L#oiE`jeH;n4@BL&pQpu*eq)~yW>MQDo4Ea>C|*L*5a)CQulY{ zdoVXS6JnkK(!m>2U`#X>(HG`oAP*=Ye(>&wNnq%==mHG5UDD2&S(>@g>`_Au8e$bZ#^n!qM_J`ZkA6iVG?b~{Dl!U== z|De!z9i1j`%@gEpdatumvZYYC|iUC^+dHupzqfyVtv4rIOoCv6F`c zb%6fp5QMXs*?ac7YNSDJdHM=#)ZV4jn=`xK&&5@yPK{X{`R_FMX}vYR+36a5?j0Gd zWL(fYZ`vQ7?xsm3z=a8qlb!)@TA054i2H6YIt%OQ_uG10rr{I2f|TqTgt6Xb9y(lU zpFMPN*N(0KPjMVEZH$>GDJFPKvS^{3ZA`mA!o~8JO|;bz<0_Dx>@i^sp4-3AX~%*F z9jjgM0OaTtAy;0(F2k#R{VNl113jWUJk6{zuN81y+D_ zBuUC|VH+zbRS0=Xf%{<%EK-)x;|GdIk|U~_(5Fz zl8fHC*kFGJZIlatb8bhJw()DLK27RY5&>1^_5c=JZ$6w#+AhCWtmJG4#`BIIgzprYX;dY8;|HTU6iHez3?f zZhldmwW7Qu>B&zf%TJ{}St_hQ^QK{>JB^n#Q2xToBRz{lN?N~{JPK<O$!mShFTwy2LZ}T$nP9%5hN0WEzJ9@5W$@{hMl2S;J)1-|jkSaIN zT8tCE=H+x+%I5W^gXTnDLq?doD(|1@ucC8_%P$05KFRqjomsG{Di_@PynO?ucYc=pHx_(jH#`k(i(1}nfMoTT=QVp^Ud_7K6p{duDAF$X&DhNE zHgo@F;u0)PF@6Duy>7^*UGosv2&3YT3eZ}ssD_=zuaku2AG7PP1M&Dkf`mJa^=;=f)#zvqU+X=*jWCgzXJ=B|iIf^5ILk(!;4*NgT+gLTd>RRIWU` z)2W`M!MY`-O1Jc8+7P z;eEk0&}g_N(>k~fm8(yuyA*7vUVNWgVPDbTlBC4hRckpBstd)D5dkZ0Uy}M z*G-7)RYz?{l6>fU{!dELUCh2yvb=+)%)) zx>-+UdUjI7_W}^foK=wb zI)0G2YC%fCkr$x7?_wrZ&sYh;&+#xP0i=K^ajb_w2IyK<{c^n58>$7r{KX__U<*6H z2qWdi>nx*fk^M6Xk6rh%j1St7eXc;8fPZ#C=xY}epjqcLR41Qz)Fs>~j1GxC!0+Rt z19a**za0DJZ^VoTXb@E*@$c9b8F(l-8N*WasBnqVX#cBQWrot?)yxI zj;=V5g#X}L(sg{4j*v~-x4%_*pC(GEsx&Ad3g2@c1W{5qj85hb-o+rlTBfD+T-EG+ zfn26~*_uPF|q24v5$Xs>#}AEsq3z`Xx~7t*+;0VTbJOC2_^4_qn0O zd0`G9-&Xgfje=JM(YYt?oia?ALk(a4c0+TAR67wdUR&b2b4$IgV>MNoJdesU6;Ym= zx$fzE)|8Q5rKOA-UPwvL?_zKkg=8S%cgnsV!6g7^!D9EBA0b1dM%_@>FX&MUn?l_V z&_{6iV$juUP-{20^4?|K26|DTyGWPo#|2303Ju8o7ZaCb=qNY5oazjrFQMr zp3U0Dt4x^Y{rlmg((@(EL5O-=+pQ+`gyWx1)6^*s>1q)=6@EsRWKZm4QpDYK$Q}-T z!m*vnc~>@Zu`9MX=8@NavW>dhBkSTh(GzlC)aWHw>z`eMHoCEI&eXJxfj z^|rMrVV;+H`IFtp&amGH<3IXD^uoiarTmLCS>(<}v>(fL5Usl;FA+mFne2b49aW8~ zFvV~Kl}3sDqVk5x2|eNPO^y4M;iHTkRtav0O=P5`Q{@qmv-?cvIr7h(e3ZCFF39hV zXI=;#B*xc#*Z~t25ash}1DZ|drM$4ZR~GidA2)9Zeeg&|1-kV3IA!hq&6|Zo4C;Ed z>?!MAT}Kyvj*q~ecyd>zdslP@^B=`UrnIVbh+|+uM4p}w-0=#)TYHxuzVv-&0qVuC zNJcWNq+DWiGq&%j_PY~FrW1jwShme_5`V|N!3~n3Y5}^h8c*(x#i=-MG??F&!hSYe zJ=3(Yw;wh|sOlniFZ%~#P(#-6s>=FfKt1(+A4++kF*9O&>1an^MXyyAUPuWeiAf~a zycS%<9%*TzW`@4zDKmR|lj=o%ctuDy|Jf>DE%>fkc&x25B_A`?LE)n6ih7n>uk07w zT`Fxxvn###<%+Q?WdsqG%ynaNDT3EHppgtU{m*`4bwul_^#~qwteeR9M|PtRT^DOCa2ooF_DnDfo8#Yir|*A! zZ9igt7uCgeX?DLZF4V~V%IrbaTWzuGG{P22^{=6|XB~}|J{N% z=ba9Jte?goy8425U8&miCod)k6N9oQbFMHZqsBX&s}!)gPuCL`EkFzF?gU!%6+#Lv zLcHeAcK7_P3R>cyr*k)03w*P^FlJR-UG_<+9XRu3WQ}q3Gw};k%eBr+a0uJBW}mt) zn#*abENv=Qx&+$tN}_4CFc(P=2_<3n7lF&p>2FME?%q45-SwV`(Tr7#BR!{u&jS$z zY-wVK3qW}d;ih4C<1U3>+bvV#>ZQ)sEA@+0nQ}{@wUIQ=Ld^8FWjR3vayx*5#yP-c z?Vi!~K}l=0%Y~TY&wTB~&-5ayi`eGh-r0`&YPESWT$r`}wntGw9=r4HcPU-ZY53KR zH#M;^lEw9YXVH>?y#`*5JBeU-?icGiy0lue#Ur2uFdIYmd}K4*$RL+W=1HOTw&evK z<15ooCSx6_uy@5x=d6@VLVM~NZiDEf5-6oI1Y7p`RUyX6{<#pNgqOY(# zahTy-TcEioa62dP4l$vplzMQNqxPFjY4zImE`RLE1RMI3CN{y&ZOWGq;rhpn7^XYj zAqr;d`0utJ=GN#54z7?7O!TaiQ{1#|xxFK?`c2CPUXhHvBYKQE2}V<~p2PFi6LKTc zpC^@wi+{%A3Q`3600T|nJ&{?X&l?HUx2R5JB?>zaT9@v8K|g%?RgIezQd?PxIQ=!Q zS{4Tp=AZC1A{T|zvltuuM>2nHm>L#HO7MQ4V%BVR1xxB};+i*68k!UNq*4CH%*L~_ zBJ18z?*B*ATR243ec!`(hM}cFN29P!=^jE5rMp48yW5~c8l;u(4$1dE zpYQMe1LmAF_nv$9-fOS54pIzeR%N8q1T8g37M00bHrLWl`|(^1LvkJUn{*-`uD8m9 z-*E5!wYF(sZr-o84Ncv;TV4`;o=$#oI{OCb^?bXVrzlb5p;2e_FUKR>^tbam7#EN#L6 z`WdIf?LEN*x6#|ag2kq(6qQmkW&X;P#v>49>-?*`>sN5;tdu1)VMFkyBIP_|*rEqV zx(BL8X;&Ca`GF>ZQxHZ9N@-Zsw&3##9P&QPx1R-uMT{ghnyYIH0&6{ev!Bsx*|=#% z!Kcjct}vHhBPm@#ESE0%Rk`rw8kxzHJLCPnSVCBPDD9Zb6D3%Z; z!Ko%>XxNoFUGvRWb0kECmmHFj5hwA`sI4umivAFp892#Jcz5^9Z$OHA)XvRxFVl~a zZLz^LxYSIEq5hxFkJx7lr;Ek-_I&mw&N6T8RoF~cqU&Jx;Oc@ zeOh7p($D-84KG&Bh&YD#NUKZ8=>a6a9JPe0S}Zf$?A#LeITSe-OD?w* z%LuAnsk;8*ayw+E? z6le8(oPXrGWsOvy;J46qLmAZtBj2V)Y<8FKka)0XpA`PE)iLC6l)VHwj8z7j@O=~h z;k+1i*w~xzosWcb4U0D*al><~j-;m~Jv+JXkmbN^S-G8j@>PA{s~3m|+ZQ*!mQ?M? z!+|&j<~ys`om*^%`58y+sJ~Z5hLdJO+nB;caXDiUkvgky23rs_p2BJKhP_hM=ViW< zWc7qCFD0I1!#l=-z2_@3ncDXaa#M7Ydg z=@+PJ>0%~8(Oorn>6LM3LkGc{CBJL~lBu_?ZAm`ws-Et7mIZD0w3G5}31Yk0`07=a zF;R1kMG3;{?wgI@$YizL{rhRx`8PmCc&T;IYmSj7j}lOQ-)l`cmrHS=Tn-cP-_Ur_VVG&^IuRVdb4Yf>4Fs_!qxk5p5=(O}^y~P{A z{fB@Pn&lq7#k6|bef|4(Q8KED#Yb%}IkfCbQ9fw`v;u1C_4&Chhw=7<<7zJ!elE1w zJQ)&C1#^Mq=j6ri=@{e}HgXA{XjzyVXy5qU?#h?a1zm6Za`+egYM0_`NdAYm)-O zmQ=SAyA0SKyJFq3y4}hIY`%^4&YeAM*3~A;2N7hr1F1b9EtcwIcs4t) zQc^m2GcHLxM(5`Z)iU7KR~~b-jX0-sl+Yq!yh;A%&?l;SQr2ChDbCG?kGE2faj2`R z>>o0oPh3}m@f3{?2oqk z=iKQK`KDIr=;_E-`6|i5$Z0vitU`}K|4ByEC@~an2Q?9$5NPYhQw_1jd`Pk8#A9@K z=iE7H5sy~1zorfe*?`mb<3B#ZX{{gppU79rlm$yzzv2Y=iC0*0)2v(zl02iJjgqw{`Iu##ZzLun5IJ!qHy*+K3K$ z^B0(w;7WE;Z}dvN42Q+sEo(%C-W~?pUas;CS}}dmn;~`t8|Fci=Dxq?O?^tWR4%`} z1#aPAQtdEjPz%=K91n&8sQO5#rc`)oulU-iM2hYZsaN~0wvPYFC@js21>)h$owp8tFqT*1kZM+XQ9p|!sDpz(;r~}ZKxS&!aZsI#G#*j1K zQ@|HT$eDdmNup>?{c;&A^LHt6><8yZ9;V^EL7KZYiXwT%W=mHcFD?U3_e#O0hOWS- zeL+o&8McUfT(k&8#{znS1$)D80KEt2`AnbKyiA zNd0p?_F*wGTwR-T_~wUe#GCS50pw|N4F=k&`*KNQYqQa1b-eC*_5mzOKyXZ$kqw$`EZY2(iB8vWA7-v2>!)MQa+Mn3T) zybCRobS=}rU`#nUtG%+jJMZ4hxvMOkZ?nnX?D`NM?mU?-xO!+iI~V`T-p12TGQFWe zfu6kAm-!l%m#r?q#Ut_?OC>Yi5S%&?#!hkkY}QA-M#|TqMP@TR;BhZZg^o%DL2ojd z{RSBP{e*n$9G*8dN*>8RMeKb%s&l98vcBuI_*{4(xD;mtG@%#>UOUeGqn&7{D3N_L zd7NMA!r9#_7Q*a8*H7f?POQePO%XOPrMxrm+;ee0oPNL^gkTBS^uo!TFupn z4N&%JXt0ApuEH%Y$#idAkNNG6Yb^bBN2;3d)wVd@HV zcrnBKxN4&1McC5uGoD>A>uzVd$^yaE+?3t6)Dc+f$P{g3b%ujvwy=z#Ig01K3&Q zi?xH(gAu_FS|>EGoX`*zQ`eoNDX^>aGpHo5B}R)6#V9A$TQL$5Hv@ z_GUNXzZw(W$}`0IZEEH%)l`$BN{(F^J??lI7)UZ7vo8G975@9RtpyX|U?O~O)}uc; z&A33zr=9}HW3lyl|R@<_x>&OEORT8_S;aS|HPFjb`2&GN7Fh${n?k9U2hZ~N`>?^?56Cq!A_ zi&cwB{~~s2`1YTqDwfve{$=!qxXqx1&qr-%Yh1YEB40%!Yx$Jd>ORUP{~Y8^M@nlO zzb@N_VvTvo(_Vj>=w`P^* zr`il$!Bo-L#6kaWVS+#%q{eTugq(IFsAnkZv79^U;4vl2%kr=qn-7i(ni#!@P7=KK z$PGIl-JtFX4BZzc*VxgleqTf&UPfV|TbQ4l!{4>r;$g$F zo$}?ZOKq+L&x?|)zFxo5Q~pmtEBB>G{UK}OfyXaMRBY6sS!=K=uVF+c?-re;{aKF` zYLBjS_2X^zVcMmE*5TvRWQ0>aJz1AkW2GG<3g}Vg_fM+1L6_gr3Kyg^>7wUm=uJ%& zE4#Ma@4MmeCHG08uGM7I+f^EjDy!zmu3TzHJ_PEj-t9b2PhL1EF@q7q5M+ZM4#!Nm zyc*}$=yVh!g`qWbN$2x$&wV7D8?c#L+B1~zx8pf#eWCa==IouQCP-;!wD#BQ*J~1J z;``6N?azO*%;oBQC+A5c-dTd<^6MeAUwgO7JbBGdNdgoGq}#x=W3$Ym-DxDYDdB%z zI!5`~*2K~87x?&3%?h$hV@aSz__1#m>UdM7rpFJ?ZwS5ZIni>{w8*uKon;NW^&GLv zDC{{uTBC=hlO4-*3-OhVRgpcNUQh22@6Ij=x)ykF%%D_C2O~gC_78{AcJ+!w<;sSl z?hiJhipsO)-Q%iSJN&_FuVm!cEL-}@7?pMO9(oaUzA!ql*pXu%K?q>}BJeEcpMiQ1XZ0;wzA zBV0^uIbk;D16A7nx?n?3V!go%Y^8QMr+(?lD^1MvW4BFI_j_Nm_=8*F(UoSSN>Ag( zE$!>3!QT{M_r7ys__@Gz->>(l>u@OXi^C21W`2LhP-fXMZIQ@y%K1owq7|F@;DEDR+;piP@4pmjgG8R<;8sXF)l?#rs?* zn=W)=!CrEq*sfH+jF(Gii%m6)jWwYCVDofGvUMs_4;0Zc+cb_5=a7)Lrxka) z_SYG_B#d}3ZBWJllCBNv*qzNe^zGU7_a7d#oI$^lpJrnv0iXlhUOK<4RGI1Th7FKa z{4H8)Q%9UsH=}cDe-4j|y4wS-{laWHtC%SAMJg|S$O{B?E?xvtPv85o3|F^h;mIb# zd|92sKtDm|q)xr?^GAm5&sbdtR;PcTsS2*soW*5;nxTlZ?Me+CS5SmHK=$_&S{TW{ zSL20~*C#sfjm5Z>#2xS+fUQ;pEH0R?o+$sAP0Hdg!D1gv=m* z$4z03C2^&V96=}{D4Cgaykr?ZaSq8Xm7~O`l`(hO;Ym&+>k!9ghrl&-kVVFvs?JRA z-lQuR`9N@X?P7Ku$Fp=f&{Pgy)_ll+st;f1DwZ&xlb z?`&J^KS5GSX+DYJC-DtFNw4$0Z=Zdp#nU&$P_*)BetPR2vWuOlPp1DZl4@`k%SBvx zROp17oT=0(1tenkUrGwT&cn;c1qOBJ+hZK@B_OSs)~Sc!m=1sIBKt4~v6}3D_EjYu zqY^$)M*dq>h9ya@=p|I97s}NOi4|F)MX`}A+U{6@6`Y=qX!dS>r4;&kG?AQM>d}iS zb<1zIKF6XSj&8$b_E*UiV1J~|5H5`7@pCj#$_SPj8HP4W{pnMv_)^Ts*zXL zTrSBr*LZ;l%CfQ}9%UUp-Uf{*u@*HaYZ2^`+MxFI%)<1fI-h=Iz-&$Cm#xC?H%N5t+-|K(m3AnWA2JNYw+(!)0#QQFY$^QTs z`<4WjBIoV!LY4yV%a6Ov%Cim=)9C|aCd^v3)K*zt1MNqur^w|YBiwb~U{CRBPvkvm z?w5*CwCAj#=f~k`$i?o$yq3kF=+d`WubLSNVwvEtY0~?W@$|W#7>4)_s6ncWMfqFb zaYL5|4a3}DLR`2sWbqLW`*)pzdjf~WqL}Qfrv8TR44s@jXuBsW&B_?ra{i?b-V4O-Z<`NW-i2*X21$Ah&$$22P8Ikd7xz6l(*|6& zz=mn6dN?r69&aLpmAcka<)g&;7X#jU`e!pwU6=z+ zx0dJO?+EG!Q$%kq3nP|Fy8U=gg&j*Pc1qpV7q|4WpdGih3@oG5T#!j?WmEWv?UIUW z?JbE1@>gMAABLt|a*;3ndy}8CxfhrsNl$Fh7seH~8T_AAJDh#?1FMk>vE2I;&}~(i z9MwWq!+X5*jU4sW@yoBXTHgS>sYc;E${3CW60(1R9W_1WS+gztz)Nc@0JLz(f?s!Y zZDGi$rCqD8K0PuPlG3dM$78*y^Escxx~=GXYRM{LYAoZKVV6vXNlH6Hv#XGucOykk ze^w~enUpKT&q!s*AE=Vj38G(ciH&Y}KV6C^x57cYr`MjyqHzCE9)W-(Mt^M~Wuvv~ z#UW}0J#W1@?=AVb+w>B|M#G8MdC2VxN_kruy2lL2deh#DFw}R*ax2;L%q-(&;{Tx7 z5tw!qpV5)@%Ux&bcA}f0?q&K}dM^9{CfLv*dpWg1#L9cgTU0gUY(F}diYqL0z9mP+ z9o%Z?v0hj`)_#7!mwu73EwEV{F2GgK`Ab60PZw5Td$%606nT>-x;Cmc%k~tlGq;9R z@Jp#Pv2L@~M`DiVwESmHS$JeXxIxlyE-Rhm7*S8_ERXM?@7EVTR3e`j#C@P5%08go zR->A%`ctIx@|CIKbL=yDW%#R#r)2T?=}1Dc#r95DIeuk9(dSjQ$rV3Lz;w5Wt?McT zNN<({+qhMGdvr^nL}N|ED!Z2e`+aIukulw{wmJH3tEqbvF}cTbIn|x2e~%PXR%O6g z+-(C7;hy_stHQ@T1F_}e*d12`N<%7IAPqez6I**JmWJz2tZINvV)rrKEp+TU{iPPW zGVf~orihpk=^T%8;fM4CRSqx3UENWpza7@172#8r?#6UzRW8X8SU=aEfbD8Xg=>C+ zz5N%qp1go?UJ22UO@tzOG0R;N3F0R*x+9+y!9(}h>+|Emh(jlcb05TcDMPE7xScQx zv)w|=V;h*Hl1irPQ&=X?6D6=z3dM6heMg&-rhY?q7RBEkPL^~EGU#>Bq)-5w6DJpV zn$lVhGr9uDeuCeopIE7f4)#!2^Nf;^1$fy>g~r)A)Fr1;3jN%RxT>>J++T`gK&6pA zuKAe8SOOKcJtD4tfW+l48AeX;XfDT(PL35XjoiB?$joGx+5Ih&8RAOarkGj%A1~<% zg1(zm_z2WJ{2Tni>-}tKQPv|DhvFsjRuG)3@-B|I_2qsAF^I65Pps!k{CQ%C8n-+! zh)GWnVk`6b7wDHfDE1pEnNFnJmkjY6De@+%_?f4kcP zLb-m+_`Ng)T{Ce(7H53JtV9iG`diqV+%4?lNOpD>va_PC?QC;aA>Um`7SAOBE&o&o zaMcJ-TfM$$O|NY4V+;mSKO=rCLG9q4l08tzY}IjW$EixbVNoA zKoe=RPm0;q*3*0saQlf=6Jw6R_O4D69#azQe?Z1R8r~zfq#7B?S@dw5Gt2rN%|fmW zPk(EBvcIT?0Uj{0-8S7tL9tsfwZ?5FDq4nO?EwD>otxz?>d1B^j0$%kD=BNs(cTPN z>`qv&Eu`->Y?~{U*}a~W<`ndJ=*wk4Yc|Co-c;(e6IQLF zf~~3fy_Dtptn6iPA3>n4&3y<{Hop`wPGY&;kOM1IibyAR$ zu6l&lAu!B9X>)Y4_4?g!qMI6HL19|0-Grh1N?u0&Ir_pdZ1}LXws!=!>T@io3{x!- z?j!50q`8~N-+iUKJjfOdGX^`l(Uo;9PMtzAOwR?1GK&waaH&=4Q1uSL>gf!fOZMzD zB`Cn!`CUL6A*eHXRfZb%H@ui#b6r5lDHofAua`w}tT&|@bi$rKk#ik;m4e)HiXQ7a z$RFqp3gG#fModm2NvyuwtcCtGvC@(-JN(&5 z>0BqYg!Ju85>_jUn)F5vZcs?iwVqTxoka>6AigzBE+4ti5f{9$-JFb1-*^amMLubZ zAfF~9sZ}8g^JpGc+!M~9{1%gPd!Sz#|E(^*@k_MpFz*Y<_@w%9S)ZNz#56L-v-&1G zYpk76q+G4uat%`SM-fltZz0L&}!iH!MSA(+Q&2H%Lsvn(%w&z!@et07W62-r1g%ukdfnzjSVGK zK$ueVgxK=Uo35{97+Mk;jP5P?9zRtmJLpYTH7#*%k2s4x_ShYFa=UhIFQvH_9qDKy z75ED|ZAh&}mf|xju_H}qvP#z1MaREsl+p*N-^!nj&iV%M`VM`?fE)PY9Fg0h`wbX^BYntD z#_@TVykUn8fj>#OAK0LG>WxIwME*5@RPC9D2%D}2S;^KTUZxu|n&_f=RS`&6%d|^NrSulHqGl#4NRSA0EQ0Y4qTt^xy%v z4BLjuC3iYjZwFdl(=+OQsVN^N||D?3&SX6)DC%ZM|>hwAT1Ay_=7Y*M?Gnh6S=wA5dJMf zQA0-m0n^NvlqF>ssWD0kbF6)yw`a^CXgTq5!(v|N>%M2ceJ(khYqz0rY&%Rg$wU`$ zNp!f81cEMoNi<7$9*IqDcfY8is`B{jt}J^4^PM53W(o`{md8ViqKlN_7$(eD@cJynzamt0kWB84tYdDznlN1-(3`r1HK0P;mT> z1RSpV`4KCbBb}RGeZf{{GW;QT(adU`5-NF1@rY?ci_2L4-k!{jc%$X?d6spxFO$0H zF;B?Jy6Q#SyNBl!BDQ6FwQo{V+jC43ruPO z>wjIe~aHw>A8olCJWipTP^-Ln(Z|=9l@GZjJZw;l}0#M+bDCeparG*D_<| zi63vQMT4t$)Go5a0cygdZ`OiYfk2HFhYS$Rn{Miwzgc81`eBJUJu)bU7HS}%nNI?V zwftZ^G|58ck7b*8cGe_edW~OPWIGYJM}ums-^BB!v5|F@{5`oMGz9sQcNiVug!Y_S zDHz*45Ch}2yfmoah?S}&zUrV{;W4MKm}5abI*B&4cCERnw3pTYEHn@TR9|7Vj{F=< z0Ir==(dWV^B?U{7n0?PLhLDXWzV2A49jnzz7l{+33dZ|)wh@s- zkR5ryZO(P8^%h)YV#Xn10>Ru*@DGCH8`?9d@fz4^Xf`DuVpo=AQ85>ggX5kDWyRHf zU}AQxP!jQ}5TheXh5pnJD#dWt(~e6dDt-$Ecm_*ZlUEzFlG=1~CqAXW$@Etx5*r@R z{h9wI_d0h5ZsgP(JhqBXM%8>z&rM~>HqrI)*BZ|!$@3FoxP}~nAnNhyq!LK6-tNjw zI1vasm1G+cJWs1F8jPW=z=gmnGUB9}KNF-Pg;5%NETQV)0io}N#5Y*2hazGDc%*1B zr!ltoj##MR$0sQ*?66!5Ql^sxH?ofCPeMuP`RrM-a*siGXmIZ+JEtjsT!T8uKh})Y z-B0FFz-{oK!`U}_lRDMwg#=#$q6*Q%O+F_^=1jP|U;qgM6+?Jwgr2xw=bHTxwf2YB z>;0u#6gbDFnF)61MVu>mu?i#=Eru3k&nlae^#ulNt<>hv`-Xmlg5*xKAl?cwXFN{K zPtA{SlZ{7IawTMdotvJ-+wamUFtT*_RVX7znGDf;iO+RDil(Sy@1%z-y9p#crA>8mk=@qp@xtyoH? zkZ9|Hmk{AH;-I&OvH<zAlIKiS+vbs2Y&&r zN(aZI@F=ebUk139Pm7uP7!Y|c{{)yG;Cj)@q+1JpRn21kdgj0oTVzri_zvUT;wukb zxo0iPW$o?7bo(bJ#kt9%vc1=6vGI7@Umm2Dgi^-C%#b;^uS>qvrD%Je=4*>ZkwyY= zmXD!oBnucmuTX;TQB`T!NY*Z?>Z1i*s;w`N2Pf6{m4;}4946ud&w9{+1W)==b30?_ z_|D7S2!LAIN}=npp%i~lb(Ghwj|V<$C>=l)bGPe!%VB-4=!#On0{&91c?hldnU~ch zer@SYrLEllSd9}|$UiZ&uXC)c=hl-bu$vn>1h!Y22qe;LTi=BHlk1WpYe-Hmv(3j{GwU1$FobI#4BhZ!U zP+Aj;QNL0xt1Qby684P9`eItm>IWZEr_Xsmt1G5N0Fu|ZkYdJ!Vd+#227{8o50KQq zdSphVTA`k7$S?n-*w9eV_8)55t)|f}A?^vQ13A#O5<&sQrL|c=I^%Eyi@iIC$7?9C ztVL+}&qi{GMKxLN3raZaVP=&mvguS9-9_IYBvQ<#W5fr;<-2;4bF9W4t|b+8UI(+F zP&HOfi&;k3QCYwokGCa9rbR7r9NmzQ;d@&Smm9+9Ik+i!-{pmFl`8I@Sfs;rubbwh z%c@j0)Ry`p5zm@6B(N6S5ox=*Elg-OqEJo;#P_nFgnQg4_=o7erw-^urP^)n)=rw` zXStkTaTzIl+C6dV5fZO60IsAz9tt+i7pwJLpRzR9&Bjo4ZX{}>KQduK zEJ&8P)b-uT)%|^lh33%GzCi_2vRzfD0F{QnF4F-L!bXZ{<7?@g3biYftKCl5?=D*+ zZx0S76(Kf_f3=G1sueHA?{;1DN#0G#QoKyIyg}e{UsLbpnbQ@vS9=lkqmo=5-{UWx`B>xX?zD`oE3D?MsXdAXrbpPb)1jnLql zkw}$`<;De_l#tg@>IJAPUi4OF;3e0sD!VPo18b{Kju2JG6Nw{I)<>!l@8_|P77JNH z>nS|w7k^goMnnNz+|WW>NBSl0%4-DEKM$n4)LhhoR5}P5ur&Q} z%1ebl=v(GHh#5w#Qob-_60n`%4}jkG@*JsK4|E$37Owd6S6kt%Id6ox$_8RWKNfp$ z@$4^=9xT%QQNWNCo*k~bpqj##ER;AST=T1|cJaM}$Hc|XMf@D^XnUcCW|3gwASy4> zj=vh<&WgjW$MwfxT6WowDb^}@B-I!7%9@qg(N5N2@%3{+T2%$X7a$i9*H3f>fd^3H zj5AsZ!uHY|s#s86oIX$fiSZSF$r(^``44kmr!qTNjsee3x;uc5{;42j=%?>3_pwK* zJ(s#Y{QzsvKg(4G^zk6{YK3&JS^i>gi#N@e9M2d`Hin{mh!~_*Gro5PGLjtR2yk%O zzqm_gB+*9M`rbJ^)1l5ip4Wk|*Ry+kP`*t0y1F64?K{JGt}g(ju=~;cc?;c}q2y0g zG?QduP=4sX;!%1;2W6zBIHoIy!A$VmMO_a%!twvTg4~dUNYxFEOCQygjTOR(22#h5 z%sXbw|0YQGjYb} zr{9l{ncIHNWa;HYg^(zIFQZjT`ilkChqX2m<`ecmYyCf9x38%naKJ@3>3i44xu@uF zR6wK#O?TX&(A$W~bdf{6@4Fhx?Js&9c~A{{!pmYxA#;O!$tdnxZZG6RNGH#P3(!JBay0N#xnG}+mb*xt;7N6! z&EQmji6Vg=KcNx$DT*W9d&|A6wJ{D_64Md??WF!h1T|}8T$*d+S7}c@%zso5l|trU zH-#+pL+RV=j|MIjxHm=~b_gcqdruxaUoneO%*s~r*{xO$@>2)W5^_F(4kL(vjgRvl zXr)aJA7gE0^y-d24avP-IuHmte<$;}O*XX+ofyf<@#FGPdVS~7?|KdoWU&x2r+3+Rbivkh{6owO5?; zIf^Q*E3g#hD8;bT_crMRu*+w^T1y1L;`B`MTyE@$I57xBr9>2Ry;adiSK$q%;f0*+ zy$7dpIRVUq;~g}GF-%PX=l|w+zMVXh8aV4p7faLliQRfSoLZ4P4I(RvnTbIWhrHkY zQH4i3>5k0IR~YC_JAU~x%Dr}l@<6tZIL3KMLJa>*73mXRJ=<}N9Y|Y5&X!ZiMV!CS zsq|^Km;bsh;R!RpkKr_<0;J{@$Fq`1U5(DO@gsRjEh4%w6&6h%52hy0t%p26*)AQ_ zZ%cRiV=4Q^b5tCASlFRdM9H3ejA~|_!JQx(f-cb&ICg3XY~dOyJb4>qbuuA$gV6W3 z3{o+vmzOLS{V}cqcg_ZTtbg)E_nOM{r)2XJWQ5MwwWw#aINq*08Pjjb+UDQ0@I69+ zK#Ci)^cPb<38|y?CDUAAx;$14G^o$UWY_Q~DJ(K?T$Ij^rpVk~O7sV^qXfSrfa$7s zJM^-{B`S4$E(pK@7KoXgQylHt(A|IA8Ho}`GX(t&n&l89f7mwPcCHrUD#DyH&N1nt zRgk4R(L__Iq=3xkSHJI6ZJt*7?T0~YdW(K1sNci*j4I^1xgdJQKNp&Oe^)M6kj(}W ze0e}qEW7Q`#OEt~N9`&n=Kd7*+Ux1}0yKL+7}#&qIbT0)?}^Hpq@n)b*M~;(QU2Hb z#;Vmm1q97&adQ#(9eOxU&ExTU?Syoum0DSqdhJSM7Qg@3!gtFvd!{hJx|9V1o>__h zDQQ(jcwVhM)r@#{C{B2NR`X<@*P5rv>B+)QZ0l@Lg=%uhtb6s!gMM3Sx`wxI#^%fM z10(<mEraonU+!Y^{_*p|Lf#5 zcb01J_P%-6@9cv1^}?_iFm?5Aem_q?#1uICZu;Bg8&ZNFb;+7#x7J~sM`Z&Isb9-; zPRY?MB@yn+EV(&v$g@aKo`JtCpt^zpeRDrMY|=eze@y<=P0tP<0MUm}Y*0YNG19h_ zXZ58Zuh!^dbi&1y+jp)l@ykFqF0oZDG_@#vHt^V^&Cqovp!?S_qE zzz0IID%y&D<4i5Gs?(d4_Wuj7PV`R)LTiaM$+P8q$Q_3e7Q)|!s;0^jJf5V(z~r3h zk&-g_B+9+}we8MY(u4myN&|uajOSPrXXr+^6`C64T_<$)!cV>ry@BL*#?y}4*l`ed zp{gQK5^n-{oab(Wu{t*@P>Zl9z<1rlg83kFQUZvWQ_^jya#fJO42-4L`h_;I&ym6q z_iji=f)pRzT3gcGiO-w4UxF8to)FPdqhV6zQG$Nmx#5&*pL5Y0ephvno+r7#!Dh9Q zj8CW92dKqV_e(3^I?@BJus1mevViU{>}NePq*m&&#qMSq0yvWP%Ck4$9hcUdenXim zmLpE}g6y!23zFYhNDj@l8(|t}n1A>$XTSk3<@x@}o9Y9h)cNN+-t2rouNSa3dTRH! zd%;Z9rN-~4_SUe}7{HRA^=L~1jyN9_1rP>(n~u9pnIadI%yf!BX1A7^@%Syp;dTJR z3eY1#qTE$NT<(-_Q5a;|{+Ab_x%?Lt=QHS(EoPqhMvDQwp1$V~OnIUAXmaj}wq}wq zwzp5~We*jW#w(2CCOOo)Rx%6JlRQWqO z=5JYz)Oj`3m~lrV4dYW%3Q4TN3HEsbU6`L#QhXQs|0b?@hpOO1`XNR73Cxv&R(nz= zH)?A4$6qgBTPeI_mm4#;ezWKQRV2uCG2bD1sZc=C(l%`4E3IQq> zUp9HX>{vS+1DvWk7ydLn zXk+BnEq$n1{m;f$87Z=|v+YDg2THFL*AHU-`Vqf>&GDQho1Du;@h_?UzkH|RuivcN zD#V$4uA{!7ZN=byT>36d$t8O3SYqCxEo=K#pu33koj4Ht<;p$aISuf~UKWAeiKEvrbDx}zg zBV%9kKH-a0xTPcW=5l*l^lej0d*%Co=0zdUYfJH56dFKJ?FUvk3FrUs1wivifcw9l z3Lc}~S*R(J-NyRWDP?_Rh&2cC02Md6WQ0F$+YA8(s9VOw6C6fT_Qn7yp&j$|eLXC6XGqY?1NrBzrOgPO;kY`0gwuQ3~|+ z`sck+E@s;lhk^4`b*cX?6yC!a8S{LI%@(XF(cU4e@cy;d^hs`R7^>v5*3pY^s7S9R zFD^zWeak%S&rSltbtnW^cm+WqsFhC0<4bLBby6z&^SQN9kFH}7=nOzZ@w+h>S>MuA z5m{(s6g%6i?YoI?#V;I-&fOxJoInsAwguz?5l;a*YKR5qta)P(L3kCjn4P5(F%=q+Enw zOtlS%`iA`MeP)^B`oNyjY?9FG;~;CskvmeG_6WPa^>8t%%;26HWkLOCbs;p21}NDp zxpEpDQn5|8_fW-Ec-#iBjbX!J z4iG7Pc{IQ>)SO~ucwX$1ihFZ~p449d7A+cpQyJyb3KE!#K;iws^+6CGkafs%S2yS) zLrM+6f6;nSXbQLNOTMOL&gvbl`#r09%+mW> zp6U#-lWNJrha%EZ2n;m3oK^PXaV7$0rmOsGnx_SS&S=7#l1jIlH2LK^wWknkdSy=j zarVCgdfUooy19pm8`I)!D$ziE0Z?-s2}-B$&`all(i0_W);T39alWdw;&DV8zZ&YX z1rBZ3eC?n@X`YQctI361G388X1OHny2}I7U=ztw-uly`eKgEXu3RGU^`~I0FZH3;% zYTMk=EYF0T*@Aab1vfW7qsqlnApB_${SO#{z}-XU+N6E{;C<4aH38YrMZ#nL74A3SiEi$w6}HGk1K`?QT4NLib`JVG zEGV^4iraDX10qV^VF<(e8}Qrq=o}#B^z2ca)*-HurT%JZ*Tw zk_vGku2ln$#rUVqR=W{Vo^se>AQJxWkXB9nhthmB4bnrbVeb$g%7stLHR(4H=v-TT> zUj*@W1b;wGsSNEr@TYzWNw|grKy5A_Ha|xv`OSu5JWiTal;GxYX|cE`=%wVx-yGqQ zKnUPP`j)`O(3AXJefD(-9#EjowO-D31<)a*p##kJ6BNTY?t6xA5B9&7|Adx-x!@Tx}m<}WW~;ig#j|Do#e{(T!WV#ZKe zn2fzx5dLDCZ-Jdg{BQLtG6N+%zQ#-;7Ib90gX!4FuAxqkUMTQ;i>2=mAwKX4eZaDt zZu`qR-egVv?O$Q#pJpKn=u1U)FZ;S+Jkzm|Upv0`@Jh@1YPTuJ!Z_v|VJV{-Z< z;(zM}p%^1$Ea#!uhIdg2xv|UCi9b(cN6s@G5$NOfFFgDDW1=aWoq2-VlWC8i$+f$bx`SIqMxx z%VuNORuAiYeH>*f>yI+;kzP{g8qai}Vn`)$amQ`g4WR=+pA8N8*>a`WrLOT|I9701qnhbfyIx^Q)t`mYwx$O||5xHn z=!=#FGJHN{RI-|#^k`t27TgkzqJks4UEBusp71#KZ$8%TtbQPMvJm*pVU5iBK9&$T zIpQ0bVB8cD4M;5vFz@iaV*~&m*jd2@e z6fPegxRT`lfy`e30!J>IKMQPJP#muh*AlLIz!40P1#@=z^Oa)?Tgj7;mEf+CWHyqeejeag%p+?|_78rZ7~zy>||9D&A?a@a_;j;KzMM{XQ`u73}7YMzJ>kMR(=0LI!kEGc(@X-&`cdJyUY)^?37|YysTL z-|uq>?Q;K73NgPm?57Js<7sFi_o%OyV*pQI$NIv$OeK^Rt=p}JX66Uq_Y62t)*?;z zMX-Vs9$~t9TeXP1C?`!(3@HGm+l@(!wr@T)_vC-pGz+>)(DvrWZoT-PsiY1;)U2^u zD$ddPKG$GQaq3e-5gC#0ep|8JyHt8g%ESjfBjwE4NO3TyVy@q*GPl6hf3BIDz{CRd zsO0K*akmoMZBUmx|1!i!`}=!ArqLGXzJprrd?@63_(AU&jLALBy@%;E^+Vk3kOn`7 zr)_Z*{-)Y{op7qnelkF;#3#YpJ1Q?Kv{N3l)JD?umINiFO!21Kc!AMSMchS3s<~}R zqY#_t>*P^#!U;Lwv3oiB{P3F)NsZp@4_?q%UwM_Hf|&o0qpM(uqV1x~(gI31(%sUH zbT`t{DJ3BwwRER+cXxM7cXue#-M!zu-w&8)V`grgbMCn%ki_VUjNNT&5));KZ^?R_+XeajJ{}aFON{N!R(#Xf~x; zA<7vyjd?kDb!`Pr96RYQ6xylI1YFRG1ZewN58VX;V=PTLt#|q;{dngT-j0ZWzqxeY zuHKa;hs8V}{zkjKFyd1m-B8{wW}A{lbljd|kTHtxK0R=TMV-qTjqnvx4H9TEp@kPy zJL}Y(8DkGx5LVTyID^XX6-j-80ImA932!rQWQt=IzJ0YWZw&)zO(qiD7z;D%2R{K@ z5#OM{i2z3%wKy6B2~ccT(|2O+r;5OhRw#sVn<+Hal=%rs9c{o`8SM2QjIDGzj|KNt#33AiY%!Z70@K9q+mj8r{6+`4{a2C_vQ>>`OvpX!IGaGY zx=!(MC-z+&E-3cruB3IJEepzbD}tWyfAQyQ7~NU`omE;rlSZI)G==E1B{Kbfp|xCo84)D@T>i%o^{LowSwUMZ)P)IPh0T?Qc{srJv==4D zh6epzLSqW-E$xipZ*COtPr~J|wtzn+BWjtkx-yX+DxE`0y|`D0g9;efaGd zVg;u3Y)&|Qn&m`moPR$>=!h1CYc0uGEsWRD8x{yo@1AX*%a8hFNrK3={a`@!*75{kkVjLQRRI_4sA$zG zjtwJItY6*>1H~TH>~j-!bTbW#tv+e4!GhsedgFqdUDNGXXRf}RG3JHB1OiactK3!{ z!G_Lsf7)+j4i&MdK2OY5ZmFhRO;vyz%H(H5p%btt&dwwGHTI1Ux5G45Ig4s8?1-*- z#aG^y-h8!6!!GspJxzg#2WHF}F5!lKJv*v-Uhkltn>xKlb`-Gi$6P#WpW{=TO!!mT zWxqS8tl)%1vf6*wB7pSn@?aMKORf?KBKv$T3{6klhluWD_<}VCu@sm;DBO}F}=p}v35I_YCr5Ncl zjN>wNESW50&25Q|7Z#spD+ANnW2o_%{lBu@vhHdf(x4vYTz}U=WrD1v=ZieI}F2^x}638Th!M z-4X*ryP-QBzEAUym%$eI3tX5qQc{#h*cl*T1(1gpcWDR+oxGO(B#U8To*_5yax(H? zfy>L0I5ZPj#_U}yRp&uLCH9HetEO&2i~7f z{lsQ_p3P)^f}=;l%(v&rgh!EL)3=x`sXf=u)P49OU%z{X?c%Mn@}nf`#9{#{-yL_mW8xZbv-gvk~tS8u4y z{i22_BQ0QMfg`! z_RZr@rNZ9>!k+ENwH!tN`axBk3Po!*+`zq#(dB#o!i1KR8t^g}<+7Wo<{~-5MPQ*+ z*tCKSA7KZk$OQWYGJ^L^J_y*@cz zss7wRpK~FH!;99#82?m2X+cAE3Uwajbb%h5>If$V63eWt@uPe&GykylVD&&8FKo%5 zuIp}?`O{g*MD_ibL_31Ro{*>%=M?77_fy6MqKIVc&;q_#POSKL?2u6r%e;kR+Vmo6 zgzK6D^m9?U4@3#7%-SSgU{p;kxc-GkKj>=;%o69|``SS?WA%qbfpx{gP?+Td#(aMY&@hr# zGmj~*N^_|8y6894kG}Fsg>Gl4J1kfz`R|HiclqOC>7OaL(tdyVGt510U)a9QtHd4o z8Dft!dKnz9o6KE3FeDBJdeA@u6E&2j7Mw=%y>s(LSkLkmtm)TtYPHO{Pra_YyMpGH zjNg8kz>x_!31;Q75UeIgjakn8bd!chjgM%XWv1l@DJE;I-jjo5h!VSa_^X~Th@ALU zr_yEZ(7+(?(}}hC)NTH&@0oQAr$?9%dMkB*FZ7{e$GfJynxnqH21*gby`uhffBA1d zxkt5@eNEiK#;Yzx$k_b$ZDZpZc5-g$gvE?&^@I|MBc8k0HGvc~&q>E0FpV0)r7?

ZQ#?Q1m6@rg^3Xlvb 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ĩB=8rAX7?Ihze|G6liE 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 literal 0 HcmV?d00001 diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/Contents.json b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIcon.imageset/Contents.json b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIcon.imageset/Contents.json new file mode 100644 index 00000000..191e70b4 --- /dev/null +++ b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "lockTabBarIcon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIcon.imageset/lockTabBarIcon.pdf b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIcon.imageset/lockTabBarIcon.pdf new file mode 100644 index 0000000000000000000000000000000000000000..aeb426b239c63ce7382efdee8be0618760990a4f GIT binary patch literal 2697 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 K3T3^&C;5Ns{Ncg? literal 0 HcmV?d00001 diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Contents.json b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Contents.json new file mode 100644 index 00000000..3a63d8f1 --- /dev/null +++ b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Near.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Near@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Near@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Near.png b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Near.png new file mode 100644 index 0000000000000000000000000000000000000000..ee9521e5ac75cb374c760df1329ce140ffff6045 GIT binary patch literal 593 zcmV-X0P000>X1^@s6#OZ}&00001b5ch_0Itp) z=>Px%3rR#lR7efImd#5QQ5c7>Auhr~`@ql=B*6}mxh0|qLh*_}3qp1}wB2TqihKKkjKXoA)|0}DgyBKQJkpuZ~NHT{#Sm_GXHTaG8`qq73ldPKmLPr<$FLHesu z4#W-pgb^F@B-d#l8i@3OtE=|xcf()U2e;)`bmemXmLJ;)NI!j(Nsef@8;In$X#w6q zdMi5Ue}TKO2}_VA{q#*H*`n>%#BvqsJB#B^7(X``aS#s#uKMYloQhAhpFq9S<#xHn zZrvheNgw_6O;q$f@p*WMuae4eBlw<}ItTUM_&enIEFMChf^WP9MZR74$#XGz%Kdg( z=+|=>hU^67iSbBh$vW%x$(1YF2H%lJWPU?+V-z&)moe|iHtjb)Z<2VcoXou*gdXsK fH1yvJ`B$$qXI*N-GvpM400000NkvXXu0mjfJwF?7 literal 0 HcmV?d00001 diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Near@2x.png b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Near@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f23864f0ff7fcc1331ef10216d1f50c4c6e96de1 GIT binary patch literal 1367 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 YFT6D`gbUVa82|tP07*qoM6N<$f*+vs=Kufz literal 0 HcmV?d00001 diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/NearSelected@2x.png b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/NearSelected@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9305aeb1540269c1fa3fe6ea2cef30d44ee8f7a0 GIT binary patch literal 1094 zcmV-M1iAZ(P)Px(07*naRA>e5n$1fVK^(_bieA8|pu!Nr0_jkMcrAKv9lAx*rEb+B>J;Ac;Gus& z5Gp$<1VMEWf}l+@lg)t@{aXH9E5*i z9zkVnl}kC5+Y1!vDl8+ajIVN+9?WWj4A*fG;RHy7v=qhwo?)4QTLoI7 zk=}&06m76R;K9pxZ1fR=i(YrFjt!(-Jo>a;+Wc4m<+a!Yp)RkmrNU)FjPSw1gGP zvR`RGh;aiR40xULC(Oc03^r#OrSwOdR~ZI7jC!8x7mT$XUV@MEIAW`3rp$Loq}d!u z+Jl&;O$P{i#l8YxrWs|lAEUoL!^YN~sQDfq=KYse*ACPNx7so|^tEF19H!-QX_w|I z%oPdS(#mIaXm=TdpP;tBP0$93`36I{T_67Lv%oX_>Z5qC>R%IQoe+D^ZA7VZ~#q1B5s z^$p_kAZOVucxg0OMBf6pU@f6%qx2b7AW&MRIT89;nTA=j;o5Jmh`vFzb-Vi7?4(gz zBbY0q#;5RZ)a$o$ig4y_RwlYMN~;wWdVL)}Xhk-_E$}V0&9ZBs)8PZTs{6aI;~))0 z-z0s|RcMx-^KlS4tA}8d9+K;s=zwKk4#UAolRm_9p%?2p7zQa)>fP)t)GvGyf73@= zq^S@F@G^ttt58t75-TtHzrT_o{ M07*qoM6N<$f(671&;S4c literal 0 HcmV?d00001 diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/NearSelected@3x.png b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/NearSelected@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..c1163b2d23bad1355a5a4988bd74277d89bf7645 GIT binary patch literal 1715 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 literal 0 HcmV?d00001 diff --git a/Xcode/SmartLock/Assets.xcassets/WirelessBlueColor.colorset/Contents.json b/Xcode/SmartLock/Assets.xcassets/WirelessBlueColor.colorset/Contents.json new file mode 100644 index 00000000..95bfff08 --- /dev/null +++ b/Xcode/SmartLock/Assets.xcassets/WirelessBlueColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.976", + "green" : "0.506", + "red" : "0.278" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} From a46ea7bbb09c68ae94ad6e2d80423c38d363dac4 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 13:37:01 -0700 Subject: [PATCH 052/229] [App] Added `UIView.configureLockAppearance()` --- iOS/LockKit/View/Appearance.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/iOS/LockKit/View/Appearance.swift b/iOS/LockKit/View/Appearance.swift index 364732be..2b5daef3 100644 --- a/iOS/LockKit/View/Appearance.swift +++ b/iOS/LockKit/View/Appearance.swift @@ -6,6 +6,7 @@ // Copyright © 2019 ColemanCDA. All rights reserved. // +#if canImport(UIKit) import Foundation import UIKit @@ -56,3 +57,4 @@ internal extension UITabBar { self.tintColor = StyleKit.wirelessBlue } } +#endif From 2697ae9d669fd634c80dba1da3656e3f52dbc818 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 13:37:27 -0700 Subject: [PATCH 053/229] [App] Added `TabBarView` --- Xcode/SmartLock/App.swift | 19 ++++++++++++-- Xcode/SmartLock/View/NearbyDevicesView.swift | 23 ++++++++++++----- Xcode/SmartLock/View/TabBarView.swift | 26 ++++++++++++++++++++ 3 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 Xcode/SmartLock/View/TabBarView.swift diff --git a/Xcode/SmartLock/App.swift b/Xcode/SmartLock/App.swift index 29b4c24b..c09f4d2c 100644 --- a/Xcode/SmartLock/App.swift +++ b/Xcode/SmartLock/App.swift @@ -11,11 +11,26 @@ import SwiftUI struct LockApp: App { //let persistenceController = PersistenceController.shared + + var body: some Scene { WindowGroup { - NearbyDevicesView() - //.environment(\.managedObjectContext, persistenceController.container.viewContext) + TabBarView() + .onAppear { + _ = LockApp.initialize + } + .onContinueUserActivity("") { _ in + + } } } + + static let initialize: () = { + // print app info + print("Launching SmartLock v\(Bundle.InfoPlist.shortVersion) Build \(Bundle.InfoPlist.version)") + + // set app appearance + UIView.configureLockAppearance() + }() } diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index 6ef2b5cf..e8fa0cde 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -18,6 +18,9 @@ struct NearbyDevicesView: View { list .navigationBarTitle(Text("Nearby"), displayMode: .automatic) .navigationBarItems(trailing: trailingButtonItem) + .tabItem { + Label("Nearby", image: "NearTabBarIconSelected") + } #elseif os(macOS) list .navigationTitle(Text("Nearby")) @@ -35,11 +38,15 @@ private extension NearbyDevicesView { } var list: some View { - ScrollView { - LazyVStack(alignment: .leading) { - ForEach(peripherals, id: \.id) { - Text(verbatim: $0.id.description) - } + List { + ForEach(peripherals, id: \.id) { + LockRowView(image: .permission(.admin), title: "\($0)") + } + } + .refreshable { + Task { + await store.scan() + try await Task.sleep(nanoseconds: 2 * 1_000_000_00) } } .task { @@ -59,6 +66,10 @@ private extension NearbyDevicesView { struct NearbyDevicesView_Previews: PreviewProvider { static var previews: some View { - NearbyDevicesView() + NavigationView { + TabView { + NearbyDevicesView() + } + } } } diff --git a/Xcode/SmartLock/View/TabBarView.swift b/Xcode/SmartLock/View/TabBarView.swift new file mode 100644 index 00000000..d83a977c --- /dev/null +++ b/Xcode/SmartLock/View/TabBarView.swift @@ -0,0 +1,26 @@ +// +// TabBarView.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/18/22. +// + +import SwiftUI + +struct TabBarView: View { + var body: some View { + NavigationView { + TabView { + NearbyDevicesView() + } + .navigationTitle("Hey") + .navigationBarTitleDisplayMode(.inline) + } + } +} + +struct TabBarView_Previews: PreviewProvider { + static var previews: some View { + TabBarView() + } +} From daa4b5f5fe46bdbe048a45d388ac90160a711fb4 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 13:37:41 -0700 Subject: [PATCH 054/229] [App] Added `SettingsView` --- Xcode/SmartLock/View/SettingsView.swift | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 Xcode/SmartLock/View/SettingsView.swift diff --git a/Xcode/SmartLock/View/SettingsView.swift b/Xcode/SmartLock/View/SettingsView.swift new file mode 100644 index 00000000..86bc7e28 --- /dev/null +++ b/Xcode/SmartLock/View/SettingsView.swift @@ -0,0 +1,28 @@ +// +// SettingsView.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/18/22. +// + +import SwiftUI + +struct SettingsView: View { + var body: some View { + Text("Settings") + .navigationBarTitle(Text("Settings"), displayMode: .automatic) + .tabItem { + Label("Settings", image: "SettingsTabBarIconSelected") + } + } +} + +struct SettingsView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + TabView { + SettingsView() + } + } + } +} From 6e30dda82be359a1e04c9f9f2479967de261c337 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 14:22:59 -0700 Subject: [PATCH 055/229] [App] Fixed Tab Bar image rendering --- .../Tab Bar Icons/LockTabBarIcon.imageset/Contents.json | 3 +++ .../LockTabBarIconSelected.imageset/Contents.json | 3 +++ .../Tab Bar Icons/NearTabBarIcon.imageset/Contents.json | 3 +++ .../NearTabBarIconSelected.imageset/Contents.json | 3 +++ .../Tab Bar Icons/SettingsTabBarIcon.imageset/Contents.json | 3 +++ .../SettingsTabBarIconSelected.imageset/Contents.json | 3 +++ 6 files changed, 18 insertions(+) diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIcon.imageset/Contents.json b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIcon.imageset/Contents.json index 191e70b4..ea94054b 100644 --- a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIcon.imageset/Contents.json +++ b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIcon.imageset/Contents.json @@ -8,5 +8,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIconSelected.imageset/Contents.json b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIconSelected.imageset/Contents.json index a8ec7143..60b6775c 100644 --- a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIconSelected.imageset/Contents.json +++ b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIconSelected.imageset/Contents.json @@ -8,5 +8,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Contents.json b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Contents.json index 3a63d8f1..59bce20c 100644 --- a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Contents.json +++ b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Contents.json @@ -19,5 +19,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/Contents.json b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/Contents.json index cadef4f8..f4b8cf67 100644 --- a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/Contents.json +++ b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/Contents.json @@ -19,5 +19,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/SettingsTabBarIcon.imageset/Contents.json b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/SettingsTabBarIcon.imageset/Contents.json index 769d51ae..cc8faa01 100644 --- a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/SettingsTabBarIcon.imageset/Contents.json +++ b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/SettingsTabBarIcon.imageset/Contents.json @@ -8,5 +8,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/SettingsTabBarIconSelected.imageset/Contents.json b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/SettingsTabBarIconSelected.imageset/Contents.json index 28d00092..ee42ee53 100644 --- a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/SettingsTabBarIconSelected.imageset/Contents.json +++ b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/SettingsTabBarIconSelected.imageset/Contents.json @@ -8,5 +8,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } From 3bce395bdc640f3ba023a547db64ffb081c84483 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 14:23:18 -0700 Subject: [PATCH 056/229] [App] Added `LockError` --- Xcode/LockKit/Model/Error.swift | 83 +++++++++++++++++++++++ Xcode/SmartLock.xcodeproj/project.pbxproj | 4 ++ 2 files changed, 87 insertions(+) create mode 100644 Xcode/LockKit/Model/Error.swift diff --git a/Xcode/LockKit/Model/Error.swift b/Xcode/LockKit/Model/Error.swift new file mode 100644 index 00000000..a5a4f297 --- /dev/null +++ b/Xcode/LockKit/Model/Error.swift @@ -0,0 +1,83 @@ +// +// Error.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/26/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation + +/// Lock app errors. +public enum LockError: Error { + + case bluetoothUnavailable + + /// The specified lock is not in range. + case notInRange(lock: UUID) + + /// 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 + + /// Invalid new key file. + case invalidNewKeyFile + + /// You already have a key for this lock. + case existingKey(lock: UUID) + + /// New key expired. + case newKeyExpired +} + +/* +// MARK: - CustomNSError + +#if os(iOS) +extension LockError: CustomNSError { + + public enum UserInfoKey: String { + + /// Lock identifier + case lock = "com.colemancda.LockKit.LockError.lock" + } + + /// The domain of the error. + public static var errorDomain: String { return "com.colemancda.LockKit.LockError" } + + /// The user-info dictionary. + public var errorUserInfo: [String : Any] { + + var userInfo = [String : Any](minimumCapacity: 2) + + switch self { + case let .notInRange(lock: lock): + userInfo[NSLocalizedDescriptionKey] = R.string.error.notInRange() + userInfo[UserInfoKey.lock.rawValue] = lock as NSUUID + 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: + userInfo[NSLocalizedDescriptionKey] = R.string.error.invalidNewKeyFile() + case let .existingKey(lock: lock): + userInfo[NSLocalizedDescriptionKey] = R.string.error.existingKey() + userInfo[UserInfoKey.lock.rawValue] = lock as NSUUID + case .newKeyExpired: + userInfo[NSLocalizedDescriptionKey] = R.string.error.newKeyExpired() + } + + return userInfo + } +} +#endif +*/ diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index afc5e238..e647853b 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 6E2182EF28D7B37000A622B3 /* TabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2182EE28D7B37000A622B3 /* TabBarView.swift */; }; 6E2182F128D7B4FA00A622B3 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2182F028D7B4FA00A622B3 /* SettingsView.swift */; }; 6E2182FA28D7B7FE00A622B3 /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2182F928D7B7FE00A622B3 /* Appearance.swift */; }; + 6E21830128D7C37500A622B3 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21830028D7C37500A622B3 /* Error.swift */; }; 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276BB28D708A000AF171B /* CoreLock */; }; 6E3276C328D70A3700AF171B /* HomeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E3276C228D70A3700AF171B /* HomeKit.framework */; }; 6E3276C628D70A3700AF171B /* RequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276C528D70A3700AF171B /* RequestHandler.swift */; }; @@ -62,6 +63,7 @@ 6E2182EE28D7B37000A622B3 /* TabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarView.swift; sourceTree = ""; }; 6E2182F028D7B4FA00A622B3 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 6E2182F928D7B7FE00A622B3 /* Appearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Appearance.swift; path = ../../../../iOS/LockKit/View/Appearance.swift; sourceTree = ""; }; + 6E21830028D7C37500A622B3 /* Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; 6E3276BA28D7088D00AF171B /* SmartLock */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SmartLock; path = ..; sourceTree = ""; }; 6E3276C128D70A3700AF171B /* MatterLock.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MatterLock.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6E3276C228D70A3700AF171B /* HomeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HomeKit.framework; path = Library/Frameworks/HomeKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -156,6 +158,7 @@ isa = PBXGroup; children = ( 6E3276D128D70CE100AF171B /* Store.swift */, + 6E21830028D7C37500A622B3 /* Error.swift */, 6E3276DB28D7195400AF171B /* Central.swift */, ); path = Model; @@ -425,6 +428,7 @@ 6E4CB61F28D7902600116573 /* NSStyleKit.swift in Sources */, 6ED248CC28D7A3BF00F78D07 /* ActivityIndicatorView.swift in Sources */, 6E4CB61028D7867700116573 /* LockRowView.swift in Sources */, + 6E21830128D7C37500A622B3 /* Error.swift in Sources */, 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */, 6E3276D228D70CE100AF171B /* Store.swift in Sources */, 6E2182FA28D7B7FE00A622B3 /* Appearance.swift in Sources */, From dcff1d72cd130f30d3463d2bffb7c5121bf0c720 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 14:24:07 -0700 Subject: [PATCH 057/229] [App] Do not scan when Bluetooth not authorized --- Xcode/LockKit/Model/Store.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index b6ac2586..62f66a9c 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -46,6 +46,10 @@ public final class Store: ObservableObject { public extension Store { func scan() async { + guard await central.state == .poweredOn else { + isScanning = false + return + } isScanning = true let filterDuplicates = true //preferences.filterDuplicates self.peripherals.removeAll(keepingCapacity: true) From 76f4a908326f25afda8640e2120c1a47c1b82b90 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 14:27:21 -0700 Subject: [PATCH 058/229] [App] Fixed file reference --- Xcode/LockKit/View/UIKit/Appearance.swift | 51 ++++++++++++++++++++ Xcode/SmartLock.xcodeproj/project.pbxproj | 8 +-- Xcode/SmartLock/View/NearbyDevicesView.swift | 22 ++++++--- Xcode/SmartLock/View/SettingsView.swift | 4 +- Xcode/SmartLock/View/TabBarView.swift | 17 +++++-- 5 files changed, 84 insertions(+), 18 deletions(-) create mode 100644 Xcode/LockKit/View/UIKit/Appearance.swift diff --git a/Xcode/LockKit/View/UIKit/Appearance.swift b/Xcode/LockKit/View/UIKit/Appearance.swift new file mode 100644 index 00000000..ad7ec58b --- /dev/null +++ b/Xcode/LockKit/View/UIKit/Appearance.swift @@ -0,0 +1,51 @@ +// +// Appearance.swift +// LockKit +// +// Created by Alsey Coleman Miller on 8/22/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +#if canImport(UIKit) +import Foundation +import UIKit + +public extension UIView { + + /// Configure the application's UI appearance + static func configureLockAppearance() { + UINavigationBar.appearance().configureLockAppearance() + UITabBar.appearance().configureLockAppearance() + } +} + +internal extension UINavigationBar { + + func configureLockAppearance() { + let barTintColor = StyleKit.wirelessBlue + let tintColor: UIColor = .white + let titleTextAttributes: [NSAttributedString.Key : Any] = [ + .foregroundColor: UIColor.white + ] + let appearance = UINavigationBarAppearance() + appearance.backgroundColor = barTintColor + appearance.titleTextAttributes = titleTextAttributes + appearance.largeTitleTextAttributes = titleTextAttributes + self.standardAppearance = appearance + self.compactAppearance = appearance + self.scrollEdgeAppearance = appearance + self.largeTitleTextAttributes = titleTextAttributes + self.titleTextAttributes = titleTextAttributes + self.barTintColor = barTintColor + self.tintColor = tintColor + } +} + +internal extension UITabBar { + + func configureLockAppearance() { + self.tintColor = StyleKit.wirelessBlue + } +} + +#endif diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index e647853b..aebdf3d5 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -9,8 +9,8 @@ /* Begin PBXBuildFile section */ 6E2182EF28D7B37000A622B3 /* TabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2182EE28D7B37000A622B3 /* TabBarView.swift */; }; 6E2182F128D7B4FA00A622B3 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2182F028D7B4FA00A622B3 /* SettingsView.swift */; }; - 6E2182FA28D7B7FE00A622B3 /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2182F928D7B7FE00A622B3 /* Appearance.swift */; }; 6E21830128D7C37500A622B3 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21830028D7C37500A622B3 /* Error.swift */; }; + 6E21830328D7C47500A622B3 /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21830228D7C47500A622B3 /* Appearance.swift */; }; 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276BB28D708A000AF171B /* CoreLock */; }; 6E3276C328D70A3700AF171B /* HomeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E3276C228D70A3700AF171B /* HomeKit.framework */; }; 6E3276C628D70A3700AF171B /* RequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276C528D70A3700AF171B /* RequestHandler.swift */; }; @@ -62,8 +62,8 @@ /* Begin PBXFileReference section */ 6E2182EE28D7B37000A622B3 /* TabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarView.swift; sourceTree = ""; }; 6E2182F028D7B4FA00A622B3 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; - 6E2182F928D7B7FE00A622B3 /* Appearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Appearance.swift; path = ../../../../iOS/LockKit/View/Appearance.swift; sourceTree = ""; }; 6E21830028D7C37500A622B3 /* Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; + 6E21830228D7C47500A622B3 /* Appearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Appearance.swift; sourceTree = ""; }; 6E3276BA28D7088D00AF171B /* SmartLock */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SmartLock; path = ..; sourceTree = ""; }; 6E3276C128D70A3700AF171B /* MatterLock.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MatterLock.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6E3276C228D70A3700AF171B /* HomeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HomeKit.framework; path = Library/Frameworks/HomeKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -180,8 +180,8 @@ children = ( 6E4CB61828D788AA00116573 /* UIStyleKit.swift */, 6E4CB62028D7907200116573 /* PermissionIconViewUIView.swift */, + 6E21830228D7C47500A622B3 /* Appearance.swift */, 6ED248CB28D7A3BF00F78D07 /* ActivityIndicatorView.swift */, - 6E2182F928D7B7FE00A622B3 /* Appearance.swift */, ); path = UIKit; sourceTree = ""; @@ -431,8 +431,8 @@ 6E21830128D7C37500A622B3 /* Error.swift in Sources */, 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */, 6E3276D228D70CE100AF171B /* Store.swift in Sources */, - 6E2182FA28D7B7FE00A622B3 /* Appearance.swift in Sources */, 6E4CB61A28D788AA00116573 /* UIStyleKit.swift in Sources */, + 6E21830328D7C47500A622B3 /* Appearance.swift in Sources */, 6E3276DC28D7195400AF171B /* Central.swift in Sources */, 6E4CB61B28D788AA00116573 /* PermissionIconView.swift in Sources */, 6E4CB62128D7907200116573 /* PermissionIconViewUIView.swift in Sources */, diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index e8fa0cde..76cecca6 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -18,9 +18,6 @@ struct NearbyDevicesView: View { list .navigationBarTitle(Text("Nearby"), displayMode: .automatic) .navigationBarItems(trailing: trailingButtonItem) - .tabItem { - Label("Nearby", image: "NearTabBarIconSelected") - } #elseif os(macOS) list .navigationTitle(Text("Nearby")) @@ -60,16 +57,27 @@ private extension NearbyDevicesView { } var trailingButtonItem: some View { - EmptyView() + if store.isScanning { + return AnyView( + ProgressView() + .progressViewStyle(.circular) + ) + } else { + return AnyView( + Button(action: { + + }, label: { + Image(systemName: "arrow.clockwise") + }) + ) + } } } struct NearbyDevicesView_Previews: PreviewProvider { static var previews: some View { NavigationView { - TabView { - NearbyDevicesView() - } + NearbyDevicesView() } } } diff --git a/Xcode/SmartLock/View/SettingsView.swift b/Xcode/SmartLock/View/SettingsView.swift index 86bc7e28..1ee5e9e0 100644 --- a/Xcode/SmartLock/View/SettingsView.swift +++ b/Xcode/SmartLock/View/SettingsView.swift @@ -20,9 +20,7 @@ struct SettingsView: View { struct SettingsView_Previews: PreviewProvider { static var previews: some View { NavigationView { - TabView { - SettingsView() - } + SettingsView() } } } diff --git a/Xcode/SmartLock/View/TabBarView.swift b/Xcode/SmartLock/View/TabBarView.swift index d83a977c..b66b2982 100644 --- a/Xcode/SmartLock/View/TabBarView.swift +++ b/Xcode/SmartLock/View/TabBarView.swift @@ -9,12 +9,21 @@ import SwiftUI struct TabBarView: View { var body: some View { - NavigationView { - TabView { + TabView { + NavigationView { NearbyDevicesView() + Text("Select a lock") + } + .tabItem { + Label("Nearby", image: "NearTabBarIconSelected") + } + NavigationView { + SettingsView() + Text("Settings detail") + } + .tabItem { + Label("Settings", image: "SettingsTabBarIconSelected") } - .navigationTitle("Hey") - .navigationBarTitleDisplayMode(.inline) } } } From 813d54084178da2716632416ea6c0b56ec308c5e Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 17:10:19 -0700 Subject: [PATCH 059/229] [App] Working on `NearbyDevicesView` --- Xcode/LockKit/Log.swift | 12 + Xcode/LockKit/Model/Permission.swift | 100 ++++++++ Xcode/LockKit/Model/Store.swift | 43 +++- Xcode/LockKit/View/UIKit/Appearance.swift | 2 +- Xcode/SmartLock.xcodeproj/project.pbxproj | 8 + .../xcshareddata/xcschemes/SmartLock.xcscheme | 78 ++++++ Xcode/SmartLock/App.swift | 10 +- Xcode/SmartLock/View/NearbyDevicesView.swift | 226 ++++++++++++++++-- 8 files changed, 441 insertions(+), 38 deletions(-) create mode 100644 Xcode/LockKit/Log.swift create mode 100644 Xcode/LockKit/Model/Permission.swift create mode 100644 Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/SmartLock.xcscheme diff --git a/Xcode/LockKit/Log.swift b/Xcode/LockKit/Log.swift new file mode 100644 index 00000000..1dd99d4a --- /dev/null +++ b/Xcode/LockKit/Log.swift @@ -0,0 +1,12 @@ +// +// Log.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/18/22. +// + +import Foundation + +public func log(_ message: String) { + NSLog(message) +} diff --git a/Xcode/LockKit/Model/Permission.swift b/Xcode/LockKit/Model/Permission.swift new file mode 100644 index 00000000..9f280c60 --- /dev/null +++ b/Xcode/LockKit/Model/Permission.swift @@ -0,0 +1,100 @@ +// +// Permission.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/18/22. +// + + +import Foundation +import CoreLock + +public extension Permission.Schedule.Interval { + + /// 9 AM to 5 PM + static var `default`: Permission.Schedule.Interval { + return Permission.Schedule.Interval(rawValue: 9 * 60 ... (12 + 5) * 60)! + } +} + +public extension PermissionType { + + var localizedText: String { + switch self { + case .owner: + return NSLocalizedString("Owner", comment: "Permission.Owner") + case .admin: + return NSLocalizedString("Admin", comment: "Permission.Admin") + case .anytime: + return NSLocalizedString("Anytime", comment: "Permission.Anytime") + case .scheduled: + return NSLocalizedString("Scheduled", comment: "Permission.Scheduled") + } + } +} + +public extension Permission { + + var localizedText: String { + + switch self { + case .owner, .admin, .anytime: + return type.localizedText + case let .scheduled(schedule): + return schedule.localizedText + } + } +} + +public extension Permission.Schedule { + + var localizedText: String { + // FIXME: Localized schedule + return NSLocalizedString("Scheduled", value: "Scheduled", comment: "Permission.Scheduled") + } +} + + +public extension Permission.Schedule.Weekdays { + + var localizedText: String { + + let every = NSLocalizedString("Every", comment: "Permission.Scheduled.Weekdays.Every") + + if self == .none { + + return NSLocalizedString("Never", comment: "Permission.Scheduled.Weekdays.Never") + + } else if self == .all { + + return String(format: "%@ %@", every, NSLocalizedString("Day", comment: "Permission.Scheduled.Weekdays.Day")) + + } else { + + return String(format: "%@ %@", every, self.days.map({ $0.localizedText }).joined(separator: ", ")) + } + } +} + +public extension Permission.Schedule.Weekdays.Day { + + var localizedText: String { + + switch self { + case .sunday: + return NSLocalizedString("Sunday", comment: "Permission.Scheduled.Weekdays.Day.Sunday") + case .monday: + return NSLocalizedString("Monday", comment: "Permission.Scheduled.Weekdays.Day.Monday") + case .tuesday: + return NSLocalizedString("Tuesday", comment: "Permission.Scheduled.Weekdays.Day.Tuesday") + case .wednesday: + return NSLocalizedString("Wednesday", comment: "Permission.Scheduled.Weekdays.Day.Wednesday") + case .thursday: + return NSLocalizedString("Thursday", comment: "Permission.Scheduled.Weekdays.Day.Thursday") + case .friday: + return NSLocalizedString("Friday", comment: "Permission.Scheduled.Weekdays.Day.Friday") + case .saturday: + return NSLocalizedString("Saturday", comment: "Permission.Scheduled.Weekdays.Day.Saturday") + } + } +} diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 62f66a9c..a12f36d8 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -20,6 +20,9 @@ public final class Store: ObservableObject { // MARK: - Properties + @Published + public internal(set) var state: DarwinBluetoothState = .unknown + @Published public internal(set) var isScanning = false @@ -36,9 +39,23 @@ public final class Store: ObservableObject { // MARK: - Initialization private init() { - _ = central + central.log = { log("📲 Central: " + $0) } + + // observe state + Task { [weak self] in + while let self = self { + let newState = await self.central.state + let oldValue = self.state + if newState != oldValue { + self.state = newState + if newState == .poweredOn { + await self.scan() + } + } + try await Task.sleep(nanoseconds: 1_000_000_000) + } + } } - } // MARK: - Bluetooth Methods @@ -51,6 +68,9 @@ public extension Store { return } isScanning = true + if let stream = scanStream, stream.isScanning { + return // already scanning + } let filterDuplicates = true //preferences.filterDuplicates self.peripherals.removeAll(keepingCapacity: true) let stream = central.scan( @@ -68,7 +88,7 @@ public extension Store { self.peripherals[scanData.peripheral] = scanData } } catch { - print("Scanning error \(error)") + log("⚠️ Unable to scan. \(error)") } isScanning = false } @@ -88,15 +108,22 @@ public extension Store { isScanning = false } - func readInformation(_ lock: DarwinCentral.Peripheral) async throws { - + @discardableResult + func readInformation(for peripheral: DarwinCentral.Peripheral) async throws -> LockInformation { + guard await central.state == .poweredOn else { + throw LockError.bluetoothUnavailable + } + // stop scanning + if isScanning { + stopScanning() + } let information = try await central.readInformation( - for: lock + for: peripheral ) - // update lock information cache - self.lockInformation[lock] = information + self.lockInformation[peripheral] = information //self[lock: information.id]?.information = LockCache.Information(information) + return information } /// Setup a lock. diff --git a/Xcode/LockKit/View/UIKit/Appearance.swift b/Xcode/LockKit/View/UIKit/Appearance.swift index ad7ec58b..d953a8d4 100644 --- a/Xcode/LockKit/View/UIKit/Appearance.swift +++ b/Xcode/LockKit/View/UIKit/Appearance.swift @@ -6,7 +6,7 @@ // Copyright © 2019 ColemanCDA. All rights reserved. // -#if canImport(UIKit) +#if canImport(UIKit) && os(iOS) import Foundation import UIKit diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index aebdf3d5..25595ad2 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 6E2182F128D7B4FA00A622B3 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2182F028D7B4FA00A622B3 /* SettingsView.swift */; }; 6E21830128D7C37500A622B3 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21830028D7C37500A622B3 /* Error.swift */; }; 6E21830328D7C47500A622B3 /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21830228D7C47500A622B3 /* Appearance.swift */; }; + 6E21830528D7C51900A622B3 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21830428D7C51900A622B3 /* Log.swift */; }; + 6E21830728D7D08300A622B3 /* Permission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21830628D7D08300A622B3 /* Permission.swift */; }; 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276BB28D708A000AF171B /* CoreLock */; }; 6E3276C328D70A3700AF171B /* HomeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E3276C228D70A3700AF171B /* HomeKit.framework */; }; 6E3276C628D70A3700AF171B /* RequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276C528D70A3700AF171B /* RequestHandler.swift */; }; @@ -64,6 +66,8 @@ 6E2182F028D7B4FA00A622B3 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 6E21830028D7C37500A622B3 /* Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; 6E21830228D7C47500A622B3 /* Appearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Appearance.swift; sourceTree = ""; }; + 6E21830428D7C51900A622B3 /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; + 6E21830628D7D08300A622B3 /* Permission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permission.swift; sourceTree = ""; }; 6E3276BA28D7088D00AF171B /* SmartLock */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SmartLock; path = ..; sourceTree = ""; }; 6E3276C128D70A3700AF171B /* MatterLock.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MatterLock.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6E3276C228D70A3700AF171B /* HomeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HomeKit.framework; path = Library/Frameworks/HomeKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -160,6 +164,7 @@ 6E3276D128D70CE100AF171B /* Store.swift */, 6E21830028D7C37500A622B3 /* Error.swift */, 6E3276DB28D7195400AF171B /* Central.swift */, + 6E21830628D7D08300A622B3 /* Permission.swift */, ); path = Model; sourceTree = ""; @@ -242,6 +247,7 @@ isa = PBXGroup; children = ( 6EA7769F28D707FE00018FA3 /* LockKit.h */, + 6E21830428D7C51900A622B3 /* Log.swift */, 6E4CB60F28D7866600116573 /* View */, 6E3276DA28D7136400AF171B /* Model */, ); @@ -427,9 +433,11 @@ files = ( 6E4CB61F28D7902600116573 /* NSStyleKit.swift in Sources */, 6ED248CC28D7A3BF00F78D07 /* ActivityIndicatorView.swift in Sources */, + 6E21830528D7C51900A622B3 /* Log.swift in Sources */, 6E4CB61028D7867700116573 /* LockRowView.swift in Sources */, 6E21830128D7C37500A622B3 /* Error.swift in Sources */, 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */, + 6E21830728D7D08300A622B3 /* Permission.swift in Sources */, 6E3276D228D70CE100AF171B /* Store.swift in Sources */, 6E4CB61A28D788AA00116573 /* UIStyleKit.swift in Sources */, 6E21830328D7C47500A622B3 /* Appearance.swift in Sources */, diff --git a/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/SmartLock.xcscheme b/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/SmartLock.xcscheme new file mode 100644 index 00000000..a8c47418 --- /dev/null +++ b/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/SmartLock.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Xcode/SmartLock/App.swift b/Xcode/SmartLock/App.swift index c09f4d2c..bbedcda7 100644 --- a/Xcode/SmartLock/App.swift +++ b/Xcode/SmartLock/App.swift @@ -6,13 +6,10 @@ // import SwiftUI +import LockKit @main struct LockApp: App { - - //let persistenceController = PersistenceController.shared - - var body: some Scene { WindowGroup { @@ -28,9 +25,10 @@ struct LockApp: App { static let initialize: () = { // print app info - print("Launching SmartLock v\(Bundle.InfoPlist.shortVersion) Build \(Bundle.InfoPlist.version)") - + log("Launching SmartLock v\(Bundle.InfoPlist.shortVersion) (\(Bundle.InfoPlist.version))") + #if canImport(UIKit) // set app appearance UIView.configureLockAppearance() + #endif }() } diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index 76cecca6..38d3ce96 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -14,19 +14,50 @@ struct NearbyDevicesView: View { var store: Store = .shared var body: some View { - #if os(iOS) - list - .navigationBarTitle(Text("Nearby"), displayMode: .automatic) - .navigationBarItems(trailing: trailingButtonItem) - #elseif os(macOS) - list - .navigationTitle(Text("Nearby")) - #endif + StateView( + state: state, + items: items, + reload: reload, + destination: { + Text(verbatim: "\($0)") + } + ) } } private extension NearbyDevicesView { + func reload() async { + guard await store.central.state == .poweredOn else { + return + } + await store.scan() + Task { + let loading = { + store.peripherals + .keys + .filter { !store.lockInformation.keys.contains($0) } + } + try? await Task.sleep(nanoseconds: 5 * 1_000_000_000) + while store.isScanning, loading().isEmpty { + try? await Task.sleep(nanoseconds: 2 * 1_000_000_000) + } + // stop scanning and load info for unknown devices + store.stopScanning() + for peripheral in loading() { + do { + let information = try await store.readInformation(for: peripheral) + log("Read information for lock \(information.id)") + #if DEBUG + dump(information) + #endif + } catch { + log("⚠️ Unable to load information for peripheral \(peripheral). \(error)") + } + } + } + } + var peripherals: [NativePeripheral] { store.peripherals .lazy @@ -34,35 +65,104 @@ private extension NearbyDevicesView { .map { $0.key } } + var state: State { + if store.state != .poweredOn { + return .bluetoothUnavailable + } else if store.isScanning { + return .scanning + } else { + return .stopScan + } + } + + var items: [Item] { + peripherals.map { item(for: $0) } + } + + func item(for peripheral: NativePeripheral) -> Item { + if let information = store.lockInformation[peripheral] { + switch information.status { + case .setup: + return .setup(peripheral.id, information.id) + default: + if let key = store.lockInformation[peripheral] { //store[lock: information.id] { + return .key(peripheral.id, "", .admin) + } else { + return .unknown(peripheral.id, information.id) + } + } + } else { + return .loading(peripheral.id) + } + } +} + +extension NearbyDevicesView { + + struct StateView : View where Destination: View { + + let state: State + + let items: [Item] + + let reload: () async -> () + + let destination: (Item) -> (Destination) + + var body: some View { + #if os(iOS) + list + .navigationBarTitle(Text("Nearby"), displayMode: .automatic) + .navigationBarItems(trailing: trailingButtonItem) + #elseif os(macOS) + list + .navigationTitle(Text("Nearby")) + #endif + } + } +} + +private extension NearbyDevicesView.StateView { + var list: some View { List { - ForEach(peripherals, id: \.id) { - LockRowView(image: .permission(.admin), title: "\($0)") + ForEach(items) { (item) in + NavigationLink(destination: { + destination(item) + }, label: { + LockRowView(item) + }) } } .refreshable { + guard state == .stopScan else { + return + } Task { - await store.scan() - try await Task.sleep(nanoseconds: 2 * 1_000_000_00) + await reload() } } .task { - await store.scan() - } - .onDisappear { - Task { - store.stopScanning() - } + await reload() } + .listStyle(.plain) } var trailingButtonItem: some View { - if store.isScanning { + switch state { + case .bluetoothUnavailable: return AnyView( - ProgressView() - .progressViewStyle(.circular) + Text(verbatim: "⚠️") ) - } else { + case .scanning: + return AnyView( + Button(action: { + + }, label: { + Image(systemName: "stop.fill") + }) + ) + case .stopScan: return AnyView( Button(action: { @@ -74,10 +174,90 @@ private extension NearbyDevicesView { } } +// MARK: - Supporting Types + +extension NearbyDevicesView { + + enum State { + case bluetoothUnavailable + case scanning + case stopScan + } +} + +extension NearbyDevicesView { + + enum Item { + case loading(NativeCentral.Peripheral.ID) + case setup(NativeCentral.Peripheral.ID, UUID) + case key(NativeCentral.Peripheral.ID, String, PermissionType) + case unknown(NativeCentral.Peripheral.ID, UUID) + } +} + +extension NearbyDevicesView.Item: Identifiable { + + var id: NativeCentral.Peripheral.ID { + switch self { + case let .loading(id): + return id + case let .setup(id, _): + return id + case let .key(id, _, _): + return id + case let .unknown(id, _): + return id + } + } +} + +extension LockRowView { + + init(_ item: NearbyDevicesView.Item) { + switch item { + case .loading: + self.init( + image: .loading, + title: "Loading..." + ) + case let .setup(_, id): + self.init( + image: .permission(.owner), + title: "Setup", + subtitle: id.description + ) + case let .unknown(_, id): + self.init( + image: .permission(.anytime), + title: "Lock", + subtitle: id.description + ) + case let .key(_, name, type): + self.init( + image: .permission(type), + title: name, + subtitle: type.localizedText + ) + } + } +} + +// MARK: - Preview + struct NearbyDevicesView_Previews: PreviewProvider { static var previews: some View { NavigationView { - NearbyDevicesView() + NearbyDevicesView.StateView( + state: .scanning, + items: [ + .loading(UUID()), + .setup(UUID(), UUID()), + .unknown(UUID(), UUID()), + .key(UUID(), "My lock", .admin) + ], + reload: { }, + destination: { Text(verbatim: $0.id.uuidString) } + ) } } } From 5e1000960f7c3aafbcfb5c061423ce24ebaf02b2 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 18:10:08 -0700 Subject: [PATCH 060/229] [App] Fixing scanning --- Xcode/LockKit/Log.swift | 4 +- Xcode/LockKit/Model/Store.swift | 5 ++- Xcode/SmartLock/App.swift | 5 ++- Xcode/SmartLock/View/NearbyDevicesView.swift | 44 ++++++++++++-------- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/Xcode/LockKit/Log.swift b/Xcode/LockKit/Log.swift index 1dd99d4a..70638962 100644 --- a/Xcode/LockKit/Log.swift +++ b/Xcode/LockKit/Log.swift @@ -8,5 +8,7 @@ import Foundation public func log(_ message: String) { - NSLog(message) + DispatchQueue.main.async { + print(message) + } } diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index a12f36d8..7c6c4ab0 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -48,8 +48,8 @@ public final class Store: ObservableObject { let oldValue = self.state if newState != oldValue { self.state = newState - if newState == .poweredOn { - await self.scan() + if newState == .poweredOn, isScanning == false { + //await self.scan() } } try await Task.sleep(nanoseconds: 1_000_000_000) @@ -71,6 +71,7 @@ public extension Store { if let stream = scanStream, stream.isScanning { return // already scanning } + self.scanStream = nil let filterDuplicates = true //preferences.filterDuplicates self.peripherals.removeAll(keepingCapacity: true) let stream = central.scan( diff --git a/Xcode/SmartLock/App.swift b/Xcode/SmartLock/App.swift index bbedcda7..532549ea 100644 --- a/Xcode/SmartLock/App.swift +++ b/Xcode/SmartLock/App.swift @@ -23,9 +23,12 @@ struct LockApp: App { } } - static let initialize: () = { + init() { // print app info log("Launching SmartLock v\(Bundle.InfoPlist.shortVersion) (\(Bundle.InfoPlist.version))") + } + + static let initialize: () = { #if canImport(UIKit) // set app appearance UIView.configureLockAppearance() diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index 38d3ce96..cdba84d1 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -17,18 +17,37 @@ struct NearbyDevicesView: View { StateView( state: state, items: items, - reload: reload, + toggleScan: toggleScan, destination: { Text(verbatim: "\($0)") } ) + .onAppear { + Task { + try await Task.sleep(nanoseconds: 2 * 1_000_000_000) + if store.isScanning == false { + await scan() + } + } + } } } private extension NearbyDevicesView { - func reload() async { - guard await store.central.state == .poweredOn else { + func toggleScan() { + if store.isScanning { + store.stopScanning() + } else { + Task { + await scan() + } + } + } + + func scan() async { + guard await store.central.state == .poweredOn, + store.isScanning == false else { return } await store.scan() @@ -105,7 +124,7 @@ extension NearbyDevicesView { let items: [Item] - let reload: () async -> () + let toggleScan: () -> () let destination: (Item) -> (Destination) @@ -134,17 +153,6 @@ private extension NearbyDevicesView.StateView { }) } } - .refreshable { - guard state == .stopScan else { - return - } - Task { - await reload() - } - } - .task { - await reload() - } .listStyle(.plain) } @@ -157,7 +165,7 @@ private extension NearbyDevicesView.StateView { case .scanning: return AnyView( Button(action: { - + toggleScan() }, label: { Image(systemName: "stop.fill") }) @@ -165,7 +173,7 @@ private extension NearbyDevicesView.StateView { case .stopScan: return AnyView( Button(action: { - + toggleScan() }, label: { Image(systemName: "arrow.clockwise") }) @@ -255,7 +263,7 @@ struct NearbyDevicesView_Previews: PreviewProvider { .unknown(UUID(), UUID()), .key(UUID(), "My lock", .admin) ], - reload: { }, + toggleScan: { }, destination: { Text(verbatim: $0.id.uuidString) } ) } From c1116b55fcfe84aa69a5d3222b3fdd8c0d67a255 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 18:24:01 -0700 Subject: [PATCH 061/229] [App] Fixed scanning --- Xcode/LockKit/Model/Store.swift | 29 +++++++++++++++++- Xcode/SmartLock/View/NearbyDevicesView.swift | 31 ++------------------ 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 7c6c4ab0..a371ea7e 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -49,7 +49,7 @@ public final class Store: ObservableObject { if newState != oldValue { self.state = newState if newState == .poweredOn, isScanning == false { - //await self.scan() + await self.scan() } } try await Task.sleep(nanoseconds: 1_000_000_000) @@ -71,6 +71,7 @@ public extension Store { if let stream = scanStream, stream.isScanning { return // already scanning } + let scanStart = Date() self.scanStream = nil let filterDuplicates = true //preferences.filterDuplicates self.peripherals.removeAll(keepingCapacity: true) @@ -79,6 +80,7 @@ public extension Store { filterDuplicates: filterDuplicates ) self.scanStream = stream + // process scanned devices Task { do { for try await scanData in stream { @@ -93,6 +95,31 @@ public extension Store { } isScanning = false } + // stop scanning after 5 sec if need to read device info + Task { + let loading = { + self.peripherals + .keys + .filter { !self.lockInformation.keys.contains($0) } + } + try? await Task.sleep(nanoseconds: 5 * 1_000_000_000) + while self.isScanning, loading().isEmpty { + try? await Task.sleep(nanoseconds: 2 * 1_000_000_000) + } + // stop scanning and load info for unknown devices + stopScanning() + for peripheral in loading() { + do { + let information = try await self.readInformation(for: peripheral) + log("Read information for lock \(information.id)") + #if DEBUG + dump(information) + #endif + } catch { + log("⚠️ Unable to load information for peripheral \(peripheral). \(error)") + } + } + } } func scan(duration: TimeInterval) async { diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index cdba84d1..157c2263 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -23,11 +23,8 @@ struct NearbyDevicesView: View { } ) .onAppear { - Task { - try await Task.sleep(nanoseconds: 2 * 1_000_000_000) - if store.isScanning == false { - await scan() - } + if store.isScanning == false { + Task { await scan() } } } } @@ -51,30 +48,6 @@ private extension NearbyDevicesView { return } await store.scan() - Task { - let loading = { - store.peripherals - .keys - .filter { !store.lockInformation.keys.contains($0) } - } - try? await Task.sleep(nanoseconds: 5 * 1_000_000_000) - while store.isScanning, loading().isEmpty { - try? await Task.sleep(nanoseconds: 2 * 1_000_000_000) - } - // stop scanning and load info for unknown devices - store.stopScanning() - for peripheral in loading() { - do { - let information = try await store.readInformation(for: peripheral) - log("Read information for lock \(information.id)") - #if DEBUG - dump(information) - #endif - } catch { - log("⚠️ Unable to load information for peripheral \(peripheral). \(error)") - } - } - } } var peripherals: [NativePeripheral] { From 0b6e027abe4aa2a42c67e63d307de8c3af532d60 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 20:55:27 -0700 Subject: [PATCH 062/229] [App] Add `ApplicationData` --- Xcode/LockKit/Model/AppGroup.swift | 14 ++ Xcode/LockKit/Model/ApplicationData.swift | 135 ++++++++++++++ Xcode/LockKit/Model/Error.swift | 2 + Xcode/LockKit/Model/FileManager.swift | 167 ++++++++++++++++++ Xcode/LockKit/Model/JSON.swift | 44 +++++ Xcode/LockKit/Model/Keychain.swift | 36 ++++ Xcode/LockKit/Model/NewKey.swift | 17 ++ Xcode/LockKit/Model/NewKeyDocument.swift | 39 ++++ Xcode/LockKit/Model/Permission.swift | 8 - Xcode/LockKit/Model/Store.swift | 134 ++++++++++++-- Xcode/SmartLock.xcodeproj/project.pbxproj | 53 +++++- .../xcshareddata/swiftpm/Package.resolved | 9 + Xcode/SmartLock/Info.plist | 12 ++ Xcode/SmartLock/SmartLock.entitlements | 22 ++- Xcode/SmartLock/View/NearbyDevicesView.swift | 4 +- iOS/LockKit/Model/Store.swift | 10 +- 16 files changed, 669 insertions(+), 37 deletions(-) create mode 100644 Xcode/LockKit/Model/AppGroup.swift create mode 100644 Xcode/LockKit/Model/ApplicationData.swift create mode 100644 Xcode/LockKit/Model/FileManager.swift create mode 100644 Xcode/LockKit/Model/JSON.swift create mode 100644 Xcode/LockKit/Model/Keychain.swift create mode 100644 Xcode/LockKit/Model/NewKey.swift create mode 100644 Xcode/LockKit/Model/NewKeyDocument.swift create mode 100644 Xcode/SmartLock/Info.plist diff --git a/Xcode/LockKit/Model/AppGroup.swift b/Xcode/LockKit/Model/AppGroup.swift new file mode 100644 index 00000000..6b9d9070 --- /dev/null +++ b/Xcode/LockKit/Model/AppGroup.swift @@ -0,0 +1,14 @@ +// +// AppGroup.swift +// LockKit +// +// Created by Alsey Coleman Miller on 8/22/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation + +public enum AppGroup: String { + + case lock = "group.com.colemancda.Lock" +} diff --git a/Xcode/LockKit/Model/ApplicationData.swift b/Xcode/LockKit/Model/ApplicationData.swift new file mode 100644 index 00000000..1d1e39e4 --- /dev/null +++ b/Xcode/LockKit/Model/ApplicationData.swift @@ -0,0 +1,135 @@ +// +// ApplicationData.swift +// LockKit +// +// Created by Alsey Coleman Miller on 8/26/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreLock + +/// Application Data. +public struct ApplicationData: Codable, Equatable { + + /// Identifier of app instance. + public let id: UUID + + /// Date application data was created. + public let created: Date + + /// Date application data was last modified. + public private(set) var updated: Date + + /// Persistent lock information. + public var locks: [UUID: LockCache] { + didSet { if locks != oldValue { didUpdate() } } + } + + /// Update date when modified. + private mutating func didUpdate() { + updated = Date() + } + + /// Initialize a new application data. + public init() { + self.id = UUID() + self.created = Date() + self.updated = Date() + self.locks = [:] + } + + public init(id: UUID, + created: Date, + updated: Date, + locks: [UUID: LockCache]) { + + self.id = id + self.created = created + self.updated = updated + self.locks = locks + } +} + +public extension ApplicationData { + + var keys: [Key] { + return locks + .values + .map { $0.key } + } + + subscript (lock id: UUID) -> LockCache? { + get { return locks[id] } + set { locks[id] = newValue } + } + + subscript (key id: UUID) -> Key? { + return locks.values + .lazy + .map { $0.key } + .first { $0.id == id } + } +} + +// MARK: - JSON + +extension ApplicationData: JSONCodable { } + +// MARK: - Supporting Types + +/// Lock Cache +public struct LockCache: Codable, Equatable { + + /// Stored key for lock. + /// + /// Can only have one key per lock. + public let key: Key + + /// User-friendly lock name + public var name: String + + /// Lock information. + public var information: Information +} + +public extension LockCache { + + /// Cached Lock Information. + struct Information: Codable, Equatable { + + /// Firmware build number + public var buildVersion: LockBuildVersion + + /// Firmware version + public var version: LockVersion + + /// Device state + public var status: LockStatus + + /// Supported lock actions + public var unlockActions: Set + } +} + +internal extension LockCache.Information { + + init(characteristic: LockInformationCharacteristic) { + + self.buildVersion = characteristic.buildVersion + self.version = characteristic.version + self.status = characteristic.status + self.unlockActions = Set(characteristic.unlockActions) + } +} + +internal extension LockCache.Information { + + init(_ lock: LockInformation) { + + self.buildVersion = lock.buildVersion + self.version = lock.version + self.status = lock.status + self.unlockActions = lock.unlockActions + } +} diff --git a/Xcode/LockKit/Model/Error.swift b/Xcode/LockKit/Model/Error.swift index a5a4f297..bfbd4697 100644 --- a/Xcode/LockKit/Model/Error.swift +++ b/Xcode/LockKit/Model/Error.swift @@ -13,6 +13,8 @@ public enum LockError: Error { case bluetoothUnavailable + case unknownLock(NativePeripheral) + /// The specified lock is not in range. case notInRange(lock: UUID) diff --git a/Xcode/LockKit/Model/FileManager.swift b/Xcode/LockKit/Model/FileManager.swift new file mode 100644 index 00000000..b389c757 --- /dev/null +++ b/Xcode/LockKit/Model/FileManager.swift @@ -0,0 +1,167 @@ +// +// FileManager.swift +// LockKit +// +// Created by Alsey Coleman Miller on 8/22/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreLock + +public extension FileManager { + + /// Returns the container directory associated with the specified security application group identifier. + func containerURL(for appGroup: AppGroup) -> URL? { + return containerURL(forSecurityApplicationGroupIdentifier: appGroup.rawValue) + } + + var cachesDirectory: URL? { + return urls(for: .cachesDirectory, in: .userDomainMask).first + } +} + +public extension FileManager { + + /// Access shared files in the Lock app group. + final class Lock { + + // MARK: - Initialization + + public static let shared = FileManager.Lock() + + private init() { } + + // MARK: - Properties + + private let jsonDecoder = JSONDecoder() + + private let jsonEncoder = JSONEncoder() + + private lazy var fileManager = FileManager() + + private lazy var containerURL: URL = { + guard let containerURL = fileManager.containerURL(for: AppGroup.lock) + else { fatalError("Could not open App Group directory"); } + return containerURL + }() + + // MARK: - Methods + + public lazy var documentURL: URL = { + guard let url = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first + else { fatalError() } + return url + }() + + public func url(for file: File) -> URL { + return containerURL.appendingPathComponent(file.rawValue) + } + + public func read(file: File) -> Data? { + return try? Data(contentsOf: url(for: file), options: [.mappedIfSafe]) + } + + public func write(_ data: Data, to file: File) throws { + try data.write(to: url(for: file), options: [.atomicWrite]) + } + } +} + +public extension FileManager.Lock { + + var applicationData: ApplicationData? { + + get { return read(ApplicationData.self, from: .applicationData) } + set { write(newValue, file: .applicationData) } + } + + @discardableResult + func save(invitation: NewKey.Invitation) throws -> URL { + + let fileName = "newKey-\(invitation.key.id).ekey" + let data = try jsonEncoder.encode(invitation) + + let fileURL = documentURL.appendingPathComponent(fileName) + guard fileManager.createFile(atPath: fileURL.path, contents: data, attributes: nil) else { + assertionFailure("Could not save file \(fileURL.path)") + throw CocoaError(.fileWriteUnknown) + } + return fileURL + } + + func loadInvitations(invalid: (URL, Error) -> ()) throws -> [URL: NewKey.Invitation] { + + let documents = try fileManager.contentsOfDirectory( + at: documentURL, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants, .skipsPackageDescendants] + ) + + let invitationURLs = documents.filter { $0.pathExtension == NewKey.Invitation.fileExtension } + guard invitationURLs.isEmpty == false + else { return [:] } + + var invitations = [URL: NewKey.Invitation](minimumCapacity: invitationURLs.count) + for url in invitationURLs { + do { + let data = try Data(contentsOf: url, options: .mappedIfSafe) + let invitation = try jsonDecoder.decode(NewKey.Invitation.self, from: data) + invitations[url] = invitation + } catch { + invalid(url, error) + } + } + return invitations + } +} + +private extension FileManager.Lock { + + func read (_ type: T.Type, from file: File) -> T? { + + guard let data = read(file: file) + else { return nil } + do { return try jsonDecoder.decode(type, from: data) } + catch { + try? fileManager.removeItem(at: url(for: file)) + #if DEBUG + print(error) + assertionFailure("Could not decode \(type) from \(file.rawValue)") + #endif + return nil + } + } + + func write (_ value: T?, file: File) { + + guard let value = value else { + do { try fileManager.removeItem(at: url(for: file)) } + catch { + #if DEBUG + print(error) + assertionFailure("Could not remove \(file.rawValue)") + #endif + } + return + } + + do { + let data = try jsonEncoder.encode(value) + try write(data, to: file) + } catch { + #if DEBUG + print(error) + assertionFailure("Could not encode \(T.self) to \(file.rawValue)") + #endif + } + } +} + +public extension FileManager.Lock { + + enum File: String { + + case applicationData = "data.json" + } +} diff --git a/Xcode/LockKit/Model/JSON.swift b/Xcode/LockKit/Model/JSON.swift new file mode 100644 index 00000000..e2299ace --- /dev/null +++ b/Xcode/LockKit/Model/JSON.swift @@ -0,0 +1,44 @@ +// +// JSON.swift +// LockKit +// +// Created by Alsey Coleman Miller on 8/26/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation + +public typealias JSONCodable = JSONEncodable & JSONDecodable + +public protocol JSONEncodable: Encodable { + + static var jsonEncoder: JSONEncoder { get } + + func encodeJSON() -> Data +} + +public extension JSONEncodable { + + static var jsonEncoder: JSONEncoder { return .init() } + + func encodeJSON() -> Data { + do { return try type(of: self).jsonEncoder.encode(self) } + catch { fatalError("Unable to encode JSON: \(error)") } + } +} + +public protocol JSONDecodable: Decodable { + + static var jsonDecoder: JSONDecoder { get } + + static func decodeJSON(from data: Data) throws -> Self +} + +public extension JSONDecodable { + + static var jsonDecoder: JSONDecoder { return .init() } + + static func decodeJSON(from data: Data) throws -> Self { + try self.jsonDecoder.decode(self, from: data) + } +} diff --git a/Xcode/LockKit/Model/Keychain.swift b/Xcode/LockKit/Model/Keychain.swift new file mode 100644 index 00000000..5c8c0591 --- /dev/null +++ b/Xcode/LockKit/Model/Keychain.swift @@ -0,0 +1,36 @@ +// +// Keychain.swift +// LockKit +// +// Created by Alsey Coleman Miller on 8/22/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import KeychainAccess + +public enum KeychainGroup: String { + + case lock = "4W79SG34MW.com.colemancda.Lock" +} + +public enum KeychainService: String { + + case lock = "com.colemancda.Lock" + case lockCloud = "com.colemancda.Lock.Cloud" +} + +public extension Keychain { + + convenience init(accessGroup: KeychainGroup) { + self.init(accessGroup: accessGroup.rawValue) + } + + convenience init(service: KeychainService) { + self.init(service: service.rawValue) + } + + convenience init(service: KeychainService, accessGroup: KeychainGroup) { + self.init(service: service.rawValue, accessGroup: accessGroup.rawValue) + } +} diff --git a/Xcode/LockKit/Model/NewKey.swift b/Xcode/LockKit/Model/NewKey.swift new file mode 100644 index 00000000..6875927f --- /dev/null +++ b/Xcode/LockKit/Model/NewKey.swift @@ -0,0 +1,17 @@ +// +// NewKey.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/26/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreLock + +public extension NewKey.Invitation { + + static let documentType = "com.colemancda.lock.key" + + static let fileExtension = "ekey" +} diff --git a/Xcode/LockKit/Model/NewKeyDocument.swift b/Xcode/LockKit/Model/NewKeyDocument.swift new file mode 100644 index 00000000..e5e6a87e --- /dev/null +++ b/Xcode/LockKit/Model/NewKeyDocument.swift @@ -0,0 +1,39 @@ +// +// NewKeyDocument.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/25/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +#if canImport(UIKit) +import Foundation +import CoreLock +import UIKit + +/// New Key Document +public final class NewKeyDocument: UIDocument { + + // MARK: - Properties + + public private(set) var invitation: NewKey.Invitation? + + private lazy var encoder = JSONEncoder() + + private lazy var decoder = JSONDecoder() + + // MARK: - Methods + + public override func contents(forType typeName: String) throws -> Any { + + guard let invitation = self.invitation else { return Data() } + return try encoder.encode(invitation) + } + + public override func load(fromContents contents: Any, ofType typeName: String?) throws { + + guard let data = contents as? Data else { return } + self.invitation = try decoder.decode(NewKey.Invitation.self, from: data) + } +} +#endif diff --git a/Xcode/LockKit/Model/Permission.swift b/Xcode/LockKit/Model/Permission.swift index 9f280c60..44e44972 100644 --- a/Xcode/LockKit/Model/Permission.swift +++ b/Xcode/LockKit/Model/Permission.swift @@ -54,23 +54,15 @@ public extension Permission.Schedule { } } - public extension Permission.Schedule.Weekdays { var localizedText: String { - let every = NSLocalizedString("Every", comment: "Permission.Scheduled.Weekdays.Every") - if self == .none { - return NSLocalizedString("Never", comment: "Permission.Scheduled.Weekdays.Never") - } else if self == .all { - return String(format: "%@ %@", every, NSLocalizedString("Day", comment: "Permission.Scheduled.Weekdays.Day")) - } else { - return String(format: "%@ %@", every, self.days.map({ $0.localizedText }).joined(separator: ", ")) } } diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index a371ea7e..6ce4a079 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -9,8 +9,9 @@ import Foundation import Combine @_exported import Bluetooth @_exported import GATT -import DarwinGATT @_exported import CoreLock +import DarwinGATT +import KeychainAccess /// Lock Store object @MainActor @@ -36,9 +37,14 @@ public final class Store: ObservableObject { private var scanStream: AsyncCentralScan? + internal lazy var keychain = Keychain(service: .lock, accessGroup: .lock) + + internal lazy var fileManager: FileManager.Lock = .shared + // MARK: - Initialization private init() { + // setup logging central.log = { log("📲 Central: " + $0) } // observe state @@ -58,10 +64,119 @@ public final class Store: ObservableObject { } } +// MARK: - Keychain + +public extension Store { + + /// Remove the specified lock from the cache and keychain. + @discardableResult + func remove(_ lock: UUID) -> Bool { + + guard let lockCache = self[lock: lock] + else { return false } + + self[lock: lock] = nil + self[key: lockCache.key.id] = nil + + return true + } + + /// Get credentials from Keychain to authorize requests. + func key(for lock: UUID) -> KeyCredentials? { + guard let cache = self[lock: lock], + let keyData = self[key: cache.key.id] + else { return nil } + return .init(id: cache.key.id, secret: keyData) + } + + func key(for peripheral: NativeCentral.Peripheral) throws -> KeyCredentials { + guard let information = self.lockInformation[peripheral] else { + throw LockError.unknownLock(peripheral) + } + guard let key = self.key(for: information.id) else { + throw LockError.noKey(lock: information.id) + } + return key + } + + /// Private Key for the specified lock. + subscript (key id: UUID) -> KeyData? { + + get { + + do { + guard let data = try keychain.getData(id.uuidString) + else { return nil } + guard let key = KeyData(data: data) + else { assertionFailure("Invalid key data"); return nil } + return key + } catch { + #if DEBUG + print(error) + #endif + assertionFailure("Unable retrieve value from keychain: \(error)") + return nil + } + } + + set { + let key = id.uuidString + do { + guard let data = newValue?.data else { + try keychain.remove(key) + return + } + if try keychain.contains(key) { + try keychain.remove(key) + } + try keychain.set(data, key: key) + } + catch { + #if DEBUG + print(error) + #endif + assertionFailure("Unable store value in keychain: \(error)") + } + } + } +} + +// MARK: - File Methods + +public extension Store { + + var applicationData: ApplicationData { + get { + if let applicationData = fileManager.applicationData { + return applicationData + } else { + let applicationData = ApplicationData() + fileManager.applicationData = applicationData + return applicationData + } + } + set { + objectWillChange.send() + fileManager.applicationData = newValue + } + } + + /// Cached information for the specified lock. + subscript (lock id: UUID) -> LockCache? { + get { return applicationData[lock: id] } + set { applicationData[lock: id] = newValue } + } +} + // MARK: - Bluetooth Methods public extension Store { + /// The Bluetooth LE peripheral for the speciifed lock. + subscript (peripheral id: UUID) -> NativeCentral.Peripheral? { + return lockInformation.first(where: { $0.value.id == id })?.key + } + func scan() async { guard await central.state == .poweredOn else { isScanning = false @@ -150,54 +265,45 @@ public extension Store { ) // update lock information cache self.lockInformation[peripheral] = information - //self[lock: information.id]?.information = LockCache.Information(information) + self[lock: information.id]?.information = LockCache.Information(information) return information } /// Setup a lock. func setup( - _ lock: DarwinCentral.Peripheral, + for lock: DarwinCentral.Peripheral, using sharedSecret: KeyData, name: String ) async throws { - let setupRequest = SetupRequest() let information = try await central.setup( setupRequest, using: sharedSecret, for: lock ) - let ownerKey = Key(setup: setupRequest) - /* let lockCache = LockCache( key: ownerKey, name: name, information: .init(information) ) - // store key self[lock: information.id] = lockCache self[key: ownerKey.id] = setupRequest.secret - */ // update lock information self.lockInformation[lock] = information } func unlock( - _ lock: DarwinCentral.Peripheral, + for lock: DarwinCentral.Peripheral, action: UnlockAction = .default ) async throws { - /* // get lock key - guard let key = self.key(for: lock) - else { return false } - + let key = try self.key(for: lock) try await central.unlock( action, using: key, for: lock ) - */ } } diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 25595ad2..40d622fd 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -13,6 +13,14 @@ 6E21830328D7C47500A622B3 /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21830228D7C47500A622B3 /* Appearance.swift */; }; 6E21830528D7C51900A622B3 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21830428D7C51900A622B3 /* Log.swift */; }; 6E21830728D7D08300A622B3 /* Permission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21830628D7D08300A622B3 /* Permission.swift */; }; + 6E21830A28D7FEA900A622B3 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 6E21830928D7FEA900A622B3 /* KeychainAccess */; }; + 6E21830C28D7FEF600A622B3 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21830B28D7FEF600A622B3 /* FileManager.swift */; }; + 6E21830E28D7FF2400A622B3 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21830D28D7FF2400A622B3 /* AppGroup.swift */; }; + 6E21831028D80DCD00A622B3 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21830F28D80DCD00A622B3 /* Keychain.swift */; }; + 6E21831328D80FDD00A622B3 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21831228D80FDD00A622B3 /* JSON.swift */; }; + 6E21831528D80FF900A622B3 /* ApplicationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21831428D80FF900A622B3 /* ApplicationData.swift */; }; + 6E21831828D8116C00A622B3 /* NewKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21831628D8116C00A622B3 /* NewKey.swift */; }; + 6E21831928D8116C00A622B3 /* NewKeyDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21831728D8116C00A622B3 /* NewKeyDocument.swift */; }; 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276BB28D708A000AF171B /* CoreLock */; }; 6E3276C328D70A3700AF171B /* HomeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E3276C228D70A3700AF171B /* HomeKit.framework */; }; 6E3276C628D70A3700AF171B /* RequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276C528D70A3700AF171B /* RequestHandler.swift */; }; @@ -68,6 +76,14 @@ 6E21830228D7C47500A622B3 /* Appearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Appearance.swift; sourceTree = ""; }; 6E21830428D7C51900A622B3 /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; 6E21830628D7D08300A622B3 /* Permission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permission.swift; sourceTree = ""; }; + 6E21830B28D7FEF600A622B3 /* FileManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; + 6E21830D28D7FF2400A622B3 /* AppGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = ""; }; + 6E21830F28D80DCD00A622B3 /* Keychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; + 6E21831128D80E3C00A622B3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 6E21831228D80FDD00A622B3 /* JSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = ""; }; + 6E21831428D80FF900A622B3 /* ApplicationData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationData.swift; sourceTree = ""; }; + 6E21831628D8116C00A622B3 /* NewKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewKey.swift; sourceTree = ""; }; + 6E21831728D8116C00A622B3 /* NewKeyDocument.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewKeyDocument.swift; sourceTree = ""; }; 6E3276BA28D7088D00AF171B /* SmartLock */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SmartLock; path = ..; sourceTree = ""; }; 6E3276C128D70A3700AF171B /* MatterLock.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MatterLock.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6E3276C228D70A3700AF171B /* HomeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HomeKit.framework; path = Library/Frameworks/HomeKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -114,6 +130,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6E21830A28D7FEA900A622B3 /* KeychainAccess in Frameworks */, 6E3276D928D70FA000AF171B /* GATT in Frameworks */, 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */, 6E3276D728D70FA000AF171B /* DarwinGATT in Frameworks */, @@ -161,10 +178,17 @@ 6E3276DA28D7136400AF171B /* Model */ = { isa = PBXGroup; children = ( - 6E3276D128D70CE100AF171B /* Store.swift */, - 6E21830028D7C37500A622B3 /* Error.swift */, + 6E21830D28D7FF2400A622B3 /* AppGroup.swift */, 6E3276DB28D7195400AF171B /* Central.swift */, + 6E21830028D7C37500A622B3 /* Error.swift */, + 6E21830F28D80DCD00A622B3 /* Keychain.swift */, + 6E21831428D80FF900A622B3 /* ApplicationData.swift */, + 6E21831628D8116C00A622B3 /* NewKey.swift */, + 6E21831728D8116C00A622B3 /* NewKeyDocument.swift */, + 6E21830B28D7FEF600A622B3 /* FileManager.swift */, + 6E21831228D80FDD00A622B3 /* JSON.swift */, 6E21830628D7D08300A622B3 /* Permission.swift */, + 6E3276D128D70CE100AF171B /* Store.swift */, ); path = Model; sourceTree = ""; @@ -225,6 +249,7 @@ 6EA7768328D7061600018FA3 /* SmartLock */ = { isa = PBXGroup; children = ( + 6E21831128D80E3C00A622B3 /* Info.plist */, 6EA7768428D7061600018FA3 /* App.swift */, 6E3276D428D70D7500AF171B /* Model */, 6E3276D328D70D6900AF171B /* View */, @@ -330,6 +355,7 @@ 6E3276BB28D708A000AF171B /* CoreLock */, 6E3276D628D70FA000AF171B /* DarwinGATT */, 6E3276D828D70FA000AF171B /* GATT */, + 6E21830928D7FEA900A622B3 /* KeychainAccess */, ); productName = LockKit; productReference = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; @@ -368,6 +394,7 @@ mainGroup = 6EA7767828D7061600018FA3; packageReferences = ( 6E3276D528D70FA000AF171B /* XCRemoteSwiftPackageReference "GATT" */, + 6E21830828D7FEA900A622B3 /* XCRemoteSwiftPackageReference "KeychainAccess" */, ); productRefGroup = 6EA7768228D7061600018FA3 /* Products */; projectDirPath = ""; @@ -432,16 +459,23 @@ buildActionMask = 2147483647; files = ( 6E4CB61F28D7902600116573 /* NSStyleKit.swift in Sources */, + 6E21831928D8116C00A622B3 /* NewKeyDocument.swift in Sources */, 6ED248CC28D7A3BF00F78D07 /* ActivityIndicatorView.swift in Sources */, 6E21830528D7C51900A622B3 /* Log.swift in Sources */, + 6E21830C28D7FEF600A622B3 /* FileManager.swift in Sources */, 6E4CB61028D7867700116573 /* LockRowView.swift in Sources */, + 6E21831828D8116C00A622B3 /* NewKey.swift in Sources */, + 6E21831028D80DCD00A622B3 /* Keychain.swift in Sources */, 6E21830128D7C37500A622B3 /* Error.swift in Sources */, 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */, 6E21830728D7D08300A622B3 /* Permission.swift in Sources */, 6E3276D228D70CE100AF171B /* Store.swift in Sources */, 6E4CB61A28D788AA00116573 /* UIStyleKit.swift in Sources */, + 6E21830E28D7FF2400A622B3 /* AppGroup.swift in Sources */, + 6E21831528D80FF900A622B3 /* ApplicationData.swift in Sources */, 6E21830328D7C47500A622B3 /* Appearance.swift in Sources */, 6E3276DC28D7195400AF171B /* Central.swift in Sources */, + 6E21831328D80FDD00A622B3 /* JSON.swift in Sources */, 6E4CB61B28D788AA00116573 /* PermissionIconView.swift in Sources */, 6E4CB62128D7907200116573 /* PermissionIconViewUIView.swift in Sources */, ); @@ -644,6 +678,7 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = SmartLock/Info.plist; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bluetooth is used to scan for nearby locks in the background."; INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Bluetooth is needed to connect to your smart lock."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; @@ -683,6 +718,7 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = SmartLock/Info.plist; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bluetooth is used to scan for nearby locks in the background."; INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Bluetooth is needed to connect to your smart lock."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; @@ -823,6 +859,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 6E21830828D7FEA900A622B3 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/MillerTechnologyPeru/KeychainAccess.git"; + requirement = { + branch = master; + kind = branch; + }; + }; 6E3276D528D70FA000AF171B /* XCRemoteSwiftPackageReference "GATT" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/PureSwift/GATT.git"; @@ -834,6 +878,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 6E21830928D7FEA900A622B3 /* KeychainAccess */ = { + isa = XCSwiftPackageProductDependency; + package = 6E21830828D7FEA900A622B3 /* XCRemoteSwiftPackageReference "KeychainAccess" */; + productName = KeychainAccess; + }; 6E3276BB28D708A000AF171B /* CoreLock */ = { isa = XCSwiftPackageProductDependency; productName = CoreLock; diff --git a/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5f7f506a..917ecfb5 100644 --- a/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,6 +27,15 @@ "revision" : "579ffd583f9a32a88e68b69e12eb85856ee165cb" } }, + { + "identity" : "keychainaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MillerTechnologyPeru/KeychainAccess.git", + "state" : { + "branch" : "master", + "revision" : "ee878eaeb3efa09753a9e29e8deb8c331b96f3c8" + } + }, { "identity" : "socket", "kind" : "remoteSourceControl", diff --git a/Xcode/SmartLock/Info.plist b/Xcode/SmartLock/Info.plist new file mode 100644 index 00000000..4d33c6e6 --- /dev/null +++ b/Xcode/SmartLock/Info.plist @@ -0,0 +1,12 @@ + + + + + UIBackgroundModes + + bluetooth-central + fetch + remote-notification + + + diff --git a/Xcode/SmartLock/SmartLock.entitlements b/Xcode/SmartLock/SmartLock.entitlements index f2ef3ae0..d32495af 100644 --- a/Xcode/SmartLock/SmartLock.entitlements +++ b/Xcode/SmartLock/SmartLock.entitlements @@ -2,9 +2,23 @@ - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.com.colemancda.Lock + + com.apple.security.device.bluetooth + + com.apple.security.device.camera + + com.apple.security.files.user-selected.read-only + + com.apple.security.personal-information.location + + keychain-access-groups + + $(AppIdentifierPrefix)com.colemancda.Lock + diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index 157c2263..2c98156f 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -77,8 +77,8 @@ private extension NearbyDevicesView { case .setup: return .setup(peripheral.id, information.id) default: - if let key = store.lockInformation[peripheral] { //store[lock: information.id] { - return .key(peripheral.id, "", .admin) + if let lockCache = store[lock: information.id] { + return .key(peripheral.id, lockCache.name, lockCache.key.permission.type) } else { return .unknown(peripheral.id, information.id) } diff --git a/iOS/LockKit/Model/Store.swift b/iOS/LockKit/Model/Store.swift index bee732dd..e02c938d 100644 --- a/iOS/LockKit/Model/Store.swift +++ b/iOS/LockKit/Model/Store.swift @@ -180,9 +180,7 @@ public final class Store: ObservableObject { else { assertionFailure("Invalid key data"); return nil } return key } catch { - #if DEBUG - print(error) - #endif + log("Unable retrieve value from keychain: \(error)") assertionFailure("Unable retrieve value from keychain: \(error)") return nil } @@ -201,10 +199,8 @@ public final class Store: ObservableObject { try keychain.set(data, key: key) } catch { - #if DEBUG - print(error) - #endif - assertionFailure("Unable store value in keychain: \(error)") + log("Unable store value in keychain: \(error)") + preconditionFailure("Unable store value in keychain: \(error)") } } } From 8e77f515dcc275f4efb86211c8fbbb475f329bc3 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 23:39:08 -0700 Subject: [PATCH 063/229] [App] Added macOS `Sidebar` --- Xcode/LockKit/Model/Store.swift | 1 + Xcode/LockKit/View/PermissionIconView.swift | 6 +- Xcode/LockKit/View/UIKit/Appearance.swift | 8 ++ Xcode/SmartLock.xcodeproj/project.pbxproj | 18 ++++- Xcode/SmartLock/App.swift | 5 +- .../AccentColor.colorset/Contents.json | 9 --- Xcode/SmartLock/View/ContentView.swift | 27 +++++++ Xcode/SmartLock/View/NearbyDevicesView.swift | 74 ++++++++++--------- Xcode/SmartLock/View/SettingsView.swift | 2 +- Xcode/SmartLock/View/Sidebar.swift | 69 +++++++++++++++++ Xcode/SmartLock/View/TabBarView.swift | 5 ++ 11 files changed, 173 insertions(+), 51 deletions(-) create mode 100644 Xcode/SmartLock/View/ContentView.swift create mode 100644 Xcode/SmartLock/View/Sidebar.swift diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 6ce4a079..cf487ae3 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -203,6 +203,7 @@ public extension Store { serviceUUIDs.contains(LockService.uuid) else { continue } // cache found device + try? await Task.sleep(nanoseconds: 200_000_000) self.peripherals[scanData.peripheral] = scanData } } catch { diff --git a/Xcode/LockKit/View/PermissionIconView.swift b/Xcode/LockKit/View/PermissionIconView.swift index fd72e753..ce3fb2e6 100644 --- a/Xcode/LockKit/View/PermissionIconView.swift +++ b/Xcode/LockKit/View/PermissionIconView.swift @@ -13,7 +13,11 @@ import CoreLock /// Renders lock permission icon. public struct PermissionIconView: View { - let permission: PermissionType + public let permission: PermissionType + + public init(permission: PermissionType) { + self.permission = permission + } } // MARK: - Preview diff --git a/Xcode/LockKit/View/UIKit/Appearance.swift b/Xcode/LockKit/View/UIKit/Appearance.swift index d953a8d4..31e262b9 100644 --- a/Xcode/LockKit/View/UIKit/Appearance.swift +++ b/Xcode/LockKit/View/UIKit/Appearance.swift @@ -27,10 +27,18 @@ internal extension UINavigationBar { let titleTextAttributes: [NSAttributedString.Key : Any] = [ .foregroundColor: UIColor.white ] + let barButtonItemAppearance = UIBarButtonItemAppearance(style: .plain) + barButtonItemAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.white] + barButtonItemAppearance.disabled.titleTextAttributes = [.foregroundColor: UIColor.lightText] + barButtonItemAppearance.highlighted.titleTextAttributes = [.foregroundColor: UIColor.label] + barButtonItemAppearance.focused.titleTextAttributes = [.foregroundColor: UIColor.white] let appearance = UINavigationBarAppearance() appearance.backgroundColor = barTintColor appearance.titleTextAttributes = titleTextAttributes appearance.largeTitleTextAttributes = titleTextAttributes + appearance.buttonAppearance = barButtonItemAppearance + appearance.backButtonAppearance = barButtonItemAppearance + appearance.doneButtonAppearance = barButtonItemAppearance self.standardAppearance = appearance self.compactAppearance = appearance self.scrollEdgeAppearance = appearance diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 40d622fd..d7e4da70 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -21,6 +21,8 @@ 6E21831528D80FF900A622B3 /* ApplicationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21831428D80FF900A622B3 /* ApplicationData.swift */; }; 6E21831828D8116C00A622B3 /* NewKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21831628D8116C00A622B3 /* NewKey.swift */; }; 6E21831928D8116C00A622B3 /* NewKeyDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21831728D8116C00A622B3 /* NewKeyDocument.swift */; }; + 6E21831B28D8341000A622B3 /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21831A28D8341000A622B3 /* Sidebar.swift */; }; + 6E21831D28D834D200A622B3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21831C28D834D200A622B3 /* ContentView.swift */; }; 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276BB28D708A000AF171B /* CoreLock */; }; 6E3276C328D70A3700AF171B /* HomeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E3276C228D70A3700AF171B /* HomeKit.framework */; }; 6E3276C628D70A3700AF171B /* RequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276C528D70A3700AF171B /* RequestHandler.swift */; }; @@ -84,6 +86,8 @@ 6E21831428D80FF900A622B3 /* ApplicationData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationData.swift; sourceTree = ""; }; 6E21831628D8116C00A622B3 /* NewKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewKey.swift; sourceTree = ""; }; 6E21831728D8116C00A622B3 /* NewKeyDocument.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewKeyDocument.swift; sourceTree = ""; }; + 6E21831A28D8341000A622B3 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = ""; }; + 6E21831C28D834D200A622B3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 6E3276BA28D7088D00AF171B /* SmartLock */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SmartLock; path = ..; sourceTree = ""; }; 6E3276C128D70A3700AF171B /* MatterLock.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MatterLock.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6E3276C228D70A3700AF171B /* HomeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HomeKit.framework; path = Library/Frameworks/HomeKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -162,6 +166,8 @@ children = ( 6E3276CF28D70C9400AF171B /* NearbyDevicesView.swift */, 6E2182EE28D7B37000A622B3 /* TabBarView.swift */, + 6E21831A28D8341000A622B3 /* Sidebar.swift */, + 6E21831C28D834D200A622B3 /* ContentView.swift */, 6E2182F028D7B4FA00A622B3 /* SettingsView.swift */, ); path = View; @@ -448,8 +454,10 @@ files = ( 6E2182F128D7B4FA00A622B3 /* SettingsView.swift in Sources */, 6E4CB62528D792BF00116573 /* InfoPlist.swift in Sources */, + 6E21831D28D834D200A622B3 /* ContentView.swift in Sources */, 6EA7768528D7061600018FA3 /* App.swift in Sources */, 6E2182EF28D7B37000A622B3 /* TabBarView.swift in Sources */, + 6E21831B28D8341000A622B3 /* Sidebar.swift in Sources */, 6E3276D028D70C9400AF171B /* NearbyDevicesView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -679,6 +687,7 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SmartLock/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bluetooth is used to scan for nearby locks in the background."; INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Bluetooth is needed to connect to your smart lock."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; @@ -693,11 +702,13 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -719,6 +730,7 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SmartLock/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bluetooth is used to scan for nearby locks in the background."; INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Bluetooth is needed to connect to your smart lock."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; @@ -733,11 +745,13 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/Xcode/SmartLock/App.swift b/Xcode/SmartLock/App.swift index 532549ea..f26709d8 100644 --- a/Xcode/SmartLock/App.swift +++ b/Xcode/SmartLock/App.swift @@ -13,7 +13,8 @@ struct LockApp: App { var body: some Scene { WindowGroup { - TabBarView() + ContentView() + .environmentObject(Store.shared) .onAppear { _ = LockApp.initialize } @@ -31,7 +32,7 @@ struct LockApp: App { static let initialize: () = { #if canImport(UIKit) // set app appearance - UIView.configureLockAppearance() + //UIView.configureLockAppearance() #endif }() } diff --git a/Xcode/SmartLock/Assets.xcassets/AccentColor.colorset/Contents.json b/Xcode/SmartLock/Assets.xcassets/AccentColor.colorset/Contents.json index 95bfff08..eb878970 100644 --- a/Xcode/SmartLock/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Xcode/SmartLock/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,15 +1,6 @@ { "colors" : [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.976", - "green" : "0.506", - "red" : "0.278" - } - }, "idiom" : "universal" } ], diff --git a/Xcode/SmartLock/View/ContentView.swift b/Xcode/SmartLock/View/ContentView.swift new file mode 100644 index 00000000..e01a2ce2 --- /dev/null +++ b/Xcode/SmartLock/View/ContentView.swift @@ -0,0 +1,27 @@ +// +// ContentView.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/18/22. +// + +import SwiftUI + +struct ContentView: View { + var body: some View { + #if os(iOS) + TabBarView() + #elseif os(macOS) + NavigationView { + Sidebar() + Text(verbatim: "Select a lock") + } + #endif + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index 2c98156f..bd5926d1 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -10,8 +10,8 @@ import LockKit struct NearbyDevicesView: View { - @ObservedObject - var store: Store = .shared + @EnvironmentObject + var store: Store var body: some View { StateView( @@ -104,11 +104,11 @@ extension NearbyDevicesView { var body: some View { #if os(iOS) list - .navigationBarTitle(Text("Nearby"), displayMode: .automatic) + .navigationTitle(title) .navigationBarItems(trailing: trailingButtonItem) #elseif os(macOS) list - .navigationTitle(Text("Nearby")) + .navigationTitle(title) #endif } } @@ -116,42 +116,44 @@ extension NearbyDevicesView { private extension NearbyDevicesView.StateView { + var title: LocalizedStringKey { + "Nearby" + } + var list: some View { List { ForEach(items) { (item) in - NavigationLink(destination: { - destination(item) - }, label: { + switch item { + case .loading, .unknown: LockRowView(item) - }) + case .key, .setup: + NavigationLink(destination: { + destination(item) + }, label: { + LockRowView(item) + }) + } } } .listStyle(.plain) } var trailingButtonItem: some View { - switch state { - case .bluetoothUnavailable: - return AnyView( - Text(verbatim: "⚠️") - ) - case .scanning: - return AnyView( - Button(action: { - toggleScan() - }, label: { - Image(systemName: "stop.fill") - }) - ) - case .stopScan: - return AnyView( - Button(action: { - toggleScan() - }, label: { - Image(systemName: "arrow.clockwise") - }) - ) - } + Button(action: { + toggleScan() + }, label: { + switch state { + case .bluetoothUnavailable: + Image(systemName: "exclamationmark.triangle.fill") + .symbolRenderingMode(.monochrome) + case .scanning: + Image(systemName: "arrow.clockwise") + .symbolRenderingMode(.monochrome) + case .stopScan: + Image(systemName: "stop.fill") + .symbolRenderingMode(.monochrome) + } + }) } } @@ -201,18 +203,18 @@ extension LockRowView { image: .loading, title: "Loading..." ) - case let .setup(_, id): - self.init( - image: .permission(.owner), - title: "Setup", - subtitle: id.description - ) case let .unknown(_, id): self.init( image: .permission(.anytime), title: "Lock", subtitle: id.description ) + case let .setup(_, id): + self.init( + image: .permission(.owner), + title: "Setup", + subtitle: id.description + ) case let .key(_, name, type): self.init( image: .permission(type), diff --git a/Xcode/SmartLock/View/SettingsView.swift b/Xcode/SmartLock/View/SettingsView.swift index 1ee5e9e0..73f4ddd6 100644 --- a/Xcode/SmartLock/View/SettingsView.swift +++ b/Xcode/SmartLock/View/SettingsView.swift @@ -10,7 +10,7 @@ import SwiftUI struct SettingsView: View { var body: some View { Text("Settings") - .navigationBarTitle(Text("Settings"), displayMode: .automatic) + .navigationTitle("Settings") .tabItem { Label("Settings", image: "SettingsTabBarIconSelected") } diff --git a/Xcode/SmartLock/View/Sidebar.swift b/Xcode/SmartLock/View/Sidebar.swift new file mode 100644 index 00000000..e003c3c4 --- /dev/null +++ b/Xcode/SmartLock/View/Sidebar.swift @@ -0,0 +1,69 @@ +// +// Sidebar.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/18/22. +// + +import SwiftUI +import LockKit + +struct Sidebar: View { + + @EnvironmentObject + var store: Store + + @State + private var isExpanded: Bool = true + + var body: some View { + List { + DisclosureGroup(isExpanded: $isExpanded, content: { + ForEach(peripherals, id: \.id) { (peripheral) in + NavigationLink(destination: { + Text(verbatim: peripheral.description) + }, label: { + Label(peripheral.description, systemImage: "antenna.radiowaves.left.and.right") + }) + } + }, label: { + HStack { + if store.isScanning { + ProgressView() + .frame(width: 16, height: 16, alignment: .leading) + } else { + Image(systemName: "antenna.radiowaves.left.and.right") + } + Text("Nearby") + } + }) + + DisclosureGroup(content: { + NavigationLink(destination: { + Text(verbatim: "Key1") + }, label: { + PermissionIconView(permission: .admin) + .frame(width: 16, height: 16, alignment: .center) + Text("Key1") + }) + }, label: { + Image(systemName: "key") + Text("Keys") + }) + + } + } +} + +private extension Sidebar { + + var peripherals: [NativePeripheral] { + store.peripherals.keys.sorted(by: { $0.id.description < $1.id.description }) + } +} + +struct Sidebar_Previews: PreviewProvider { + static var previews: some View { + Sidebar() + } +} diff --git a/Xcode/SmartLock/View/TabBarView.swift b/Xcode/SmartLock/View/TabBarView.swift index b66b2982..2a57ce88 100644 --- a/Xcode/SmartLock/View/TabBarView.swift +++ b/Xcode/SmartLock/View/TabBarView.swift @@ -5,7 +5,9 @@ // Created by Alsey Coleman Miller on 9/18/22. // +#if os(iOS) import SwiftUI +import LockKit struct TabBarView: View { var body: some View { @@ -25,6 +27,8 @@ struct TabBarView: View { Label("Settings", image: "SettingsTabBarIconSelected") } } + .navigationViewStyle(.stack) + .navigationBarTitleDisplayMode(.large) } } @@ -33,3 +37,4 @@ struct TabBarView_Previews: PreviewProvider { TabBarView() } } +#endif From b01e08b60d0d765895683cebdf40d86fc0d59387 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 23:40:15 -0700 Subject: [PATCH 064/229] Ignore macOS files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a43ac435..25280357 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ build/ DerivedData/ ## Various settings +*.DS_Store *.pbxuser !default.pbxuser *.mode1v3 From 46e4fb7de28affb05f525ae86ccb74111c5df357 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 00:39:23 -0700 Subject: [PATCH 065/229] [App] Added `ProgressIndicatorView` --- .../View/AppKit/ProgressIndicatorView.swift | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 Xcode/LockKit/View/AppKit/ProgressIndicatorView.swift diff --git a/Xcode/LockKit/View/AppKit/ProgressIndicatorView.swift b/Xcode/LockKit/View/AppKit/ProgressIndicatorView.swift new file mode 100644 index 00000000..c5ec18ab --- /dev/null +++ b/Xcode/LockKit/View/AppKit/ProgressIndicatorView.swift @@ -0,0 +1,49 @@ +// +// ProgressIndicatorView.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/19/22. +// + +#if canImport(AppKit) +import SwiftUI +import AppKit + +public struct ProgressIndicatorView: View, NSViewRepresentable { + + public let style: NSProgressIndicator.Style + + public let controlSize: NSControl.ControlSize + + public func makeNSView(context: Context) -> NSProgressIndicator { + let view = NSProgressIndicator(frame: NSRect(x: 0, y: 0, width: 48, height: 48)) + configure(view) + view.startAnimation(nil) + return view + } + + public func updateNSView(_ view: NSProgressIndicator, context: Context) { + configure(view) + } + + private func configure(_ view: NSProgressIndicator) { + view.style = self.style + view.controlSize = self.controlSize + } +} + +struct ProgressIndicatorView_Previews: PreviewProvider { + static var previews: some View { + VStack(spacing: 20) { + ProgressIndicatorView(style: .bar, controlSize: .regular) + ProgressIndicatorView(style: .bar, controlSize: .mini) + .frame(width: 150, height: 32, alignment: .center) + ProgressIndicatorView(style: .spinning, controlSize: .large) + ProgressIndicatorView(style: .spinning, controlSize: .regular) + ProgressIndicatorView(style: .spinning, controlSize: .small) + ProgressIndicatorView(style: .spinning, controlSize: .mini) + } + .padding(20) + } +} +#endif From 0b9d6471a8d69ad3241dc04702ef28bc4e763f44 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 00:42:13 -0700 Subject: [PATCH 066/229] [App] Added `SidebarLabel` --- Xcode/SmartLock/View/SidebarLabel.swift | 104 ++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 Xcode/SmartLock/View/SidebarLabel.swift diff --git a/Xcode/SmartLock/View/SidebarLabel.swift b/Xcode/SmartLock/View/SidebarLabel.swift new file mode 100644 index 00000000..314d8aca --- /dev/null +++ b/Xcode/SmartLock/View/SidebarLabel.swift @@ -0,0 +1,104 @@ +// +// SidebarLabel.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/18/22. +// + +#if os(macOS) +import SwiftUI +import LockKit + +struct SidebarLabel: View { + + let title: String + + let image: Image + + var body: some View { + HStack(alignment: .center, spacing: 3) { + ImageView(image: image) + .frame(width: 22, height: 22, alignment: .center) + Text(verbatim: title) + } + } +} + +extension SidebarLabel { + + enum Image { + case loading + case permission(PermissionType) + case emoji(Character) + case symbol(String) + } +} + +extension SidebarLabel { + + struct ImageView: View { + + let image: SidebarLabel.Image + + var body: some View { + switch image { + case .loading: + AnyView( + ProgressIndicatorView(style: .spinning, controlSize: .small) + ) + case let .permission(permission): + AnyView( + PermissionIconView(permission: permission) + .frame(width: 16, height: 16, alignment: .center) + ) + case let .emoji(emoji): + AnyView( + Text(verbatim: String(emoji)) + .font(.system(size: 12)) + ) + case let .symbol(symbol): + AnyView( + SwiftUI.Image(systemName: symbol) + .font(.system(size: 15)) + ) + } + } + } +} + +// MARK: - Preview + +struct SidebarLabel_Previews: PreviewProvider { + static var previews: some View { + List { + DisclosureGroup(isExpanded: State(initialValue: true).projectedValue, content: { + SidebarLabel(title: "Setup", image: .permission(.owner)) + SidebarLabel(title: "Lock 1", image: .permission(.admin)) + SidebarLabel(title: "Lock 2", image: .permission(.anytime)) + SidebarLabel(title: "Lock 3", image: .permission(.scheduled)) + SidebarLabel(title: "Lock", image: .permission(.anytime)) + SidebarLabel(title: "⚠️ History ", image: .emoji("⚠️")) + }, label: { + SidebarLabel(title: "Loading...", image: .loading) + }) + DisclosureGroup(isExpanded: State(initialValue: true).projectedValue, content: { + SidebarLabel(title: "Setup", image: .permission(.owner)) + SidebarLabel(title: "Lock 1", image: .permission(.admin)) + SidebarLabel(title: "Lock 2", image: .permission(.anytime)) + SidebarLabel(title: "Lock 3", image: .permission(.scheduled)) + SidebarLabel(title: "Lock", image: .permission(.anytime)) + }, label: { + SidebarLabel(title: "Nearby", image: .symbol("antenna.radiowaves.left.and.right")) + }) + DisclosureGroup(content: { + SidebarLabel(title: "Setup", image: .permission(.owner)) + SidebarLabel(title: "Lock 1", image: .permission(.admin)) + SidebarLabel(title: "Lock 2", image: .permission(.anytime)) + SidebarLabel(title: "Lock 2", image: .permission(.scheduled)) + }, label: { + SidebarLabel(title: "Keys", image: .symbol("key")) + }) + } + } +} +#endif From 687bc110c43d9b947861dc5909209e3a5dba1406 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 01:34:33 -0700 Subject: [PATCH 067/229] [App] Added `SidebarView` --- .../View/AppKit/ProgressIndicatorView.swift | 5 + Xcode/LockKit/View/LockRowView.swift | 10 +- Xcode/SmartLock.xcodeproj/project.pbxproj | 16 +- Xcode/SmartLock/View/ContentView.swift | 7 +- Xcode/SmartLock/View/Sidebar.swift | 69 ------- Xcode/SmartLock/View/SidebarView.swift | 179 ++++++++++++++++++ 6 files changed, 202 insertions(+), 84 deletions(-) delete mode 100644 Xcode/SmartLock/View/Sidebar.swift create mode 100644 Xcode/SmartLock/View/SidebarView.swift diff --git a/Xcode/LockKit/View/AppKit/ProgressIndicatorView.swift b/Xcode/LockKit/View/AppKit/ProgressIndicatorView.swift index c5ec18ab..6d5aac49 100644 --- a/Xcode/LockKit/View/AppKit/ProgressIndicatorView.swift +++ b/Xcode/LockKit/View/AppKit/ProgressIndicatorView.swift @@ -30,6 +30,11 @@ public struct ProgressIndicatorView: View, NSViewRepresentable { view.style = self.style view.controlSize = self.controlSize } + + public init(style: NSProgressIndicator.Style, controlSize: NSControl.ControlSize) { + self.style = style + self.controlSize = controlSize + } } struct ProgressIndicatorView_Previews: PreviewProvider { diff --git a/Xcode/LockKit/View/LockRowView.swift b/Xcode/LockKit/View/LockRowView.swift index d5c9ee31..f81c9ba6 100644 --- a/Xcode/LockKit/View/LockRowView.swift +++ b/Xcode/LockKit/View/LockRowView.swift @@ -45,6 +45,7 @@ public struct LockRowView: View { } } } + .padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)) } public init( @@ -131,19 +132,14 @@ struct LockRowView_Previews: PreviewProvider { title: "Loading..." ) LockRowView( - image: .permission(.admin), + image: .permission(.owner), title: "Setup", subtitle: "D39FE551-523F-4F64-96FC-4B828A1F8561" ) LockRowView( image: .permission(.admin), title: "Lock Name", - subtitle: "Anytime" - ) - LockRowView( - image: .permission(.owner), - title: "My house", - subtitle: "Owner" + subtitle: "Admin" ) LockRowView( image: .permission(.anytime), diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index d7e4da70..045a32d3 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -21,8 +21,10 @@ 6E21831528D80FF900A622B3 /* ApplicationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21831428D80FF900A622B3 /* ApplicationData.swift */; }; 6E21831828D8116C00A622B3 /* NewKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21831628D8116C00A622B3 /* NewKey.swift */; }; 6E21831928D8116C00A622B3 /* NewKeyDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21831728D8116C00A622B3 /* NewKeyDocument.swift */; }; - 6E21831B28D8341000A622B3 /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21831A28D8341000A622B3 /* Sidebar.swift */; }; + 6E21831B28D8341000A622B3 /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21831A28D8341000A622B3 /* SidebarView.swift */; }; 6E21831D28D834D200A622B3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21831C28D834D200A622B3 /* ContentView.swift */; }; + 6E21831F28D84A7300A622B3 /* SidebarLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21831E28D84A7300A622B3 /* SidebarLabel.swift */; }; + 6E21832128D8501500A622B3 /* ProgressIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21832028D8501500A622B3 /* ProgressIndicatorView.swift */; }; 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276BB28D708A000AF171B /* CoreLock */; }; 6E3276C328D70A3700AF171B /* HomeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E3276C228D70A3700AF171B /* HomeKit.framework */; }; 6E3276C628D70A3700AF171B /* RequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276C528D70A3700AF171B /* RequestHandler.swift */; }; @@ -86,8 +88,10 @@ 6E21831428D80FF900A622B3 /* ApplicationData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationData.swift; sourceTree = ""; }; 6E21831628D8116C00A622B3 /* NewKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewKey.swift; sourceTree = ""; }; 6E21831728D8116C00A622B3 /* NewKeyDocument.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewKeyDocument.swift; sourceTree = ""; }; - 6E21831A28D8341000A622B3 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = ""; }; + 6E21831A28D8341000A622B3 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = ""; }; 6E21831C28D834D200A622B3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 6E21831E28D84A7300A622B3 /* SidebarLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarLabel.swift; sourceTree = ""; }; + 6E21832028D8501500A622B3 /* ProgressIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressIndicatorView.swift; sourceTree = ""; }; 6E3276BA28D7088D00AF171B /* SmartLock */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SmartLock; path = ..; sourceTree = ""; }; 6E3276C128D70A3700AF171B /* MatterLock.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MatterLock.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6E3276C228D70A3700AF171B /* HomeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HomeKit.framework; path = Library/Frameworks/HomeKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -166,7 +170,8 @@ children = ( 6E3276CF28D70C9400AF171B /* NearbyDevicesView.swift */, 6E2182EE28D7B37000A622B3 /* TabBarView.swift */, - 6E21831A28D8341000A622B3 /* Sidebar.swift */, + 6E21831A28D8341000A622B3 /* SidebarView.swift */, + 6E21831E28D84A7300A622B3 /* SidebarLabel.swift */, 6E21831C28D834D200A622B3 /* ContentView.swift */, 6E2182F028D7B4FA00A622B3 /* SettingsView.swift */, ); @@ -226,6 +231,7 @@ children = ( 6E4CB61E28D7902600116573 /* NSStyleKit.swift */, 6E4CB62228D7907E00116573 /* PermissionIconViewNSView.swift */, + 6E21832028D8501500A622B3 /* ProgressIndicatorView.swift */, ); path = AppKit; sourceTree = ""; @@ -457,8 +463,9 @@ 6E21831D28D834D200A622B3 /* ContentView.swift in Sources */, 6EA7768528D7061600018FA3 /* App.swift in Sources */, 6E2182EF28D7B37000A622B3 /* TabBarView.swift in Sources */, - 6E21831B28D8341000A622B3 /* Sidebar.swift in Sources */, + 6E21831B28D8341000A622B3 /* SidebarView.swift in Sources */, 6E3276D028D70C9400AF171B /* NearbyDevicesView.swift in Sources */, + 6E21831F28D84A7300A622B3 /* SidebarLabel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -484,6 +491,7 @@ 6E21830328D7C47500A622B3 /* Appearance.swift in Sources */, 6E3276DC28D7195400AF171B /* Central.swift in Sources */, 6E21831328D80FDD00A622B3 /* JSON.swift in Sources */, + 6E21832128D8501500A622B3 /* ProgressIndicatorView.swift in Sources */, 6E4CB61B28D788AA00116573 /* PermissionIconView.swift in Sources */, 6E4CB62128D7907200116573 /* PermissionIconViewUIView.swift in Sources */, ); diff --git a/Xcode/SmartLock/View/ContentView.swift b/Xcode/SmartLock/View/ContentView.swift index e01a2ce2..3621aaa0 100644 --- a/Xcode/SmartLock/View/ContentView.swift +++ b/Xcode/SmartLock/View/ContentView.swift @@ -12,14 +12,13 @@ struct ContentView: View { #if os(iOS) TabBarView() #elseif os(macOS) - NavigationView { - Sidebar() - Text(verbatim: "Select a lock") - } + SidebarView() #endif } } +// MARK: - Preview + struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() diff --git a/Xcode/SmartLock/View/Sidebar.swift b/Xcode/SmartLock/View/Sidebar.swift deleted file mode 100644 index e003c3c4..00000000 --- a/Xcode/SmartLock/View/Sidebar.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// Sidebar.swift -// SmartLock -// -// Created by Alsey Coleman Miller on 9/18/22. -// - -import SwiftUI -import LockKit - -struct Sidebar: View { - - @EnvironmentObject - var store: Store - - @State - private var isExpanded: Bool = true - - var body: some View { - List { - DisclosureGroup(isExpanded: $isExpanded, content: { - ForEach(peripherals, id: \.id) { (peripheral) in - NavigationLink(destination: { - Text(verbatim: peripheral.description) - }, label: { - Label(peripheral.description, systemImage: "antenna.radiowaves.left.and.right") - }) - } - }, label: { - HStack { - if store.isScanning { - ProgressView() - .frame(width: 16, height: 16, alignment: .leading) - } else { - Image(systemName: "antenna.radiowaves.left.and.right") - } - Text("Nearby") - } - }) - - DisclosureGroup(content: { - NavigationLink(destination: { - Text(verbatim: "Key1") - }, label: { - PermissionIconView(permission: .admin) - .frame(width: 16, height: 16, alignment: .center) - Text("Key1") - }) - }, label: { - Image(systemName: "key") - Text("Keys") - }) - - } - } -} - -private extension Sidebar { - - var peripherals: [NativePeripheral] { - store.peripherals.keys.sorted(by: { $0.id.description < $1.id.description }) - } -} - -struct Sidebar_Previews: PreviewProvider { - static var previews: some View { - Sidebar() - } -} diff --git a/Xcode/SmartLock/View/SidebarView.swift b/Xcode/SmartLock/View/SidebarView.swift new file mode 100644 index 00000000..1abd1735 --- /dev/null +++ b/Xcode/SmartLock/View/SidebarView.swift @@ -0,0 +1,179 @@ +// +// Sidebar.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/18/22. +// + +#if os(macOS) +import SwiftUI +import LockKit + +struct SidebarView: View { + + @EnvironmentObject + var store: Store + + @State + var selection: Item.ID? + + @State + private var isNearbyExpanded = true + + @State + private var isKeysExpanded = true + + var body: some View { + SwiftUI.NavigationView { + SidebarView.NavigationView( + selection: $selection, + isScanning: store.isScanning, + locks: locks, + keys: keys, + isNearbyExpanded: Binding( + get: { isNearbyExpanded }, + set: { toggleNearbyExpanded($0) } + ), + isKeysExpanded: $isKeysExpanded + ) + Text(verbatim: "Select a lock\n\(selection?.description ?? "")") + } + } +} + +private extension SidebarView { + + var peripherals: [NativePeripheral] { + store.peripherals.keys.sorted(by: { $0.id.description < $1.id.description }) + } + + var locks: [Item] { + peripherals.map { + .lock($0.id, "Lock \($0)", .admin) + } + } + + var keys: [Item] { + [ + .key(UUID(), "Key1", .admin) + ] + } + + func toggleNearbyExpanded(_ newValue: Bool) { + isNearbyExpanded = newValue + // start scanning if not already + if newValue, !store.isScanning { + Task { + try? await Task.sleep(nanoseconds: 1_000_000_000) + await store.scan() + } + } else if !newValue, store.isScanning { + store.stopScanning() + } + } +} + +extension SidebarView { + + struct NavigationView: View { + + @Binding + var selection: Item.ID? + + let isScanning: Bool + + let locks: [Item] + + let keys: [Item] + + @Binding + var isNearbyExpanded: Bool + + @Binding + var isKeysExpanded: Bool + + var body: some View { + List(selection: $selection) { + Group( + title: "Nearby", + image: isScanning ? .loading : .symbol("antenna.radiowaves.left.and.right"), + items: locks, + isExpanded: $isNearbyExpanded + ) + Group( + title: "Keys", + image: .symbol("key"), + items: keys, + isExpanded: $isKeysExpanded + ) + } + } + } +} + +extension SidebarView { + + struct Group: View { + + let title: String + + let image: SidebarLabel.Image + + let items: [Item] + + let isExpanded: Binding + + var body: some View { + DisclosureGroup(isExpanded: isExpanded, content: { + ForEach(items) { + SidebarLabel($0) + } + }, label: { + SidebarLabel(title: title, image: image) + }) + } + } +} + +// MARK: - Supporting Types + +extension SidebarView { + + enum Item { + case lock(NativePeripheral.ID, String, PermissionType) + case key(UUID, String, PermissionType) + } +} + +extension SidebarView.Item: Identifiable { + + var id: String { + switch self { + case let .lock(id, _, _): + return "lock_" + id.description + case let .key(id, _, _): + return "key_" + id.description + } + } +} + +extension SidebarLabel { + + init(_ item: SidebarView.Item) { + switch item { + case let .lock(_, title, permission): + self.init(title: title, image: .permission(permission)) + case let .key(_, title, permission): + self.init(title: title, image: .permission(permission)) + } + } +} + +// MARK: - Preview + +struct Sidebar_Previews: PreviewProvider { + static var previews: some View { + EmptyView() + } +} +#endif From 32d25975f4188e771f0dee64482def329b66bb84 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 02:05:35 -0700 Subject: [PATCH 068/229] [App] Fixed `SidebarView` keys list --- Xcode/LockKit/Model/Store.swift | 3 +- Xcode/SmartLock/View/SidebarView.swift | 47 +++++++++++++++++++++----- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index cf487ae3..6255da09 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -25,7 +25,7 @@ public final class Store: ObservableObject { public internal(set) var state: DarwinBluetoothState = .unknown @Published - public internal(set) var isScanning = false + public var isScanning = false @Published public var peripherals = [NativePeripheral: ScanData]() @@ -186,7 +186,6 @@ public extension Store { if let stream = scanStream, stream.isScanning { return // already scanning } - let scanStart = Date() self.scanStream = nil let filterDuplicates = true //preferences.filterDuplicates self.peripherals.removeAll(keepingCapacity: true) diff --git a/Xcode/SmartLock/View/SidebarView.swift b/Xcode/SmartLock/View/SidebarView.swift index 1abd1735..56ff0759 100644 --- a/Xcode/SmartLock/View/SidebarView.swift +++ b/Xcode/SmartLock/View/SidebarView.swift @@ -48,21 +48,23 @@ private extension SidebarView { } var locks: [Item] { - peripherals.map { - .lock($0.id, "Lock \($0)", .admin) - } + peripherals.map { item(for: $0) } } var keys: [Item] { - [ - .key(UUID(), "Key1", .admin) - ] + store.applicationData.locks.values + .lazy + .map { $0.key } + .sorted(by: { $0.created < $1.created }) + .map { .key($0.id, $0.name, $0.permission.type) } } func toggleNearbyExpanded(_ newValue: Bool) { isNearbyExpanded = newValue // start scanning if not already if newValue, !store.isScanning { + store.peripherals.removeAll(keepingCapacity: true) + store.isScanning = true Task { try? await Task.sleep(nanoseconds: 1_000_000_000) await store.scan() @@ -71,6 +73,27 @@ private extension SidebarView { store.stopScanning() } } + + func item(for peripheral: NativePeripheral) -> Item { + if let information = store.lockInformation[peripheral] { + switch information.status { + case .setup: + //return .setup(peripheral.id, information.id) + return .lock(peripheral.id, "Setup", .owner) + default: + if let lockCache = store[lock: information.id] { + //return .key(peripheral.id, lockCache.name, lockCache.key.permission.type) + return .lock(peripheral.id, lockCache.name, lockCache.key.permission.type) + } else { + //return .unknown(peripheral.id, information.id) + return .lock(peripheral.id, "Lock", .anytime) + } + } + } else { + //return .loading(peripheral.id) + return .lock(peripheral.id, "Loading...", nil) + } + } } extension SidebarView { @@ -140,7 +163,7 @@ extension SidebarView { extension SidebarView { enum Item { - case lock(NativePeripheral.ID, String, PermissionType) + case lock(NativePeripheral.ID, String, PermissionType?) case key(UUID, String, PermissionType) } } @@ -162,9 +185,15 @@ extension SidebarLabel { init(_ item: SidebarView.Item) { switch item { case let .lock(_, title, permission): - self.init(title: title, image: .permission(permission)) + self.init( + title: title, + image: permission.flatMap { .permission($0) } ?? .loading + ) case let .key(_, title, permission): - self.init(title: title, image: .permission(permission)) + self.init( + title: title, + image: .permission(permission) + ) } } } From 6a90cab21c29bd3f64638e2c4ed90bd5a1fee12e Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 02:54:37 -0700 Subject: [PATCH 069/229] [App] Fixed `NearbyDevicesView` scan button --- Xcode/SmartLock/View/NearbyDevicesView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index bd5926d1..484cb70a 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -147,10 +147,10 @@ private extension NearbyDevicesView.StateView { Image(systemName: "exclamationmark.triangle.fill") .symbolRenderingMode(.monochrome) case .scanning: - Image(systemName: "arrow.clockwise") + Image(systemName: "stop.fill") .symbolRenderingMode(.monochrome) case .stopScan: - Image(systemName: "stop.fill") + Image(systemName: "arrow.clockwise") .symbolRenderingMode(.monochrome) } }) From b84b42ce1ff955007f5dae8526a79291169bb8dc Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 02:55:11 -0700 Subject: [PATCH 070/229] [App] Added `SidebarView.DetailView` --- Xcode/SmartLock/View/SidebarView.swift | 52 +++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/Xcode/SmartLock/View/SidebarView.swift b/Xcode/SmartLock/View/SidebarView.swift index 56ff0759..11674f3e 100644 --- a/Xcode/SmartLock/View/SidebarView.swift +++ b/Xcode/SmartLock/View/SidebarView.swift @@ -36,7 +36,7 @@ struct SidebarView: View { ), isKeysExpanded: $isKeysExpanded ) - Text(verbatim: "Select a lock\n\(selection?.description ?? "")") + DetailView(state: detail) } } } @@ -94,6 +94,56 @@ private extension SidebarView { return .lock(peripheral.id, "Loading...", nil) } } + + var detail: DetailView.State { + guard let selection = self.selection, let item = locks.first(where: { $0.id == selection }) ?? keys.first(where: { $0.id == selection }) else { + return .empty + } + switch item { + case let .lock(id, _, _): + // FIXME: store peripheral instead of id + guard let peripheral = store.peripherals.keys.first(where: { $0.id == id }) else { + return .empty + } + return .lock(peripheral) + case let .key(id, _, _): + return .key(id) + } + } +} + +extension SidebarView { + + struct DetailView: View { + + let state: State + + var body: some View { + switch state { + case .empty: + AnyView( + Text("Select a lock") + ) + case let .lock(peripheral): + AnyView( + Text(verbatim: "Lock \(peripheral)") + ) + case let .key(id): + AnyView( + Text(verbatim: "Key \(id)") + ) + } + } + } +} + +extension SidebarView.DetailView { + + enum State { + case empty + case lock(NativePeripheral) + case key(UUID) + } } extension SidebarView { From 99ed384a920f064d656e02b127b5c8c5c7cb6ded Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 09:32:26 -0700 Subject: [PATCH 071/229] [App] Implemented new key request in `Store` --- Xcode/LockKit/Model/Store.swift | 188 +++++++++++++++++++++++++++++++- 1 file changed, 187 insertions(+), 1 deletion(-) diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 6255da09..5f4233a3 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -217,7 +217,7 @@ public extension Store { .keys .filter { !self.lockInformation.keys.contains($0) } } - try? await Task.sleep(nanoseconds: 5 * 1_000_000_000) + try? await Task.sleep(nanoseconds: 3 * 1_000_000_000) while self.isScanning, loading().isEmpty { try? await Task.sleep(nanoseconds: 2 * 1_000_000_000) } @@ -266,6 +266,7 @@ public extension Store { // update lock information cache self.lockInformation[peripheral] = information self[lock: information.id]?.information = LockCache.Information(information) + log("Read information for \(information.id)") return information } @@ -292,6 +293,7 @@ public extension Store { self[key: ownerKey.id] = setupRequest.secret // update lock information self.lockInformation[lock] = information + log("Finished setup for \(information.id)") } func unlock( @@ -305,5 +307,189 @@ public extension Store { using: key, for: lock ) + log("Unlocked") + } + + func newKey( + for peripheral: DarwinCentral.Peripheral, + permission: Permission, + name newKeyName: String + ) async throws -> NewKey.Invitation { + // get lock key + guard let information = self.lockInformation[peripheral] else { + throw LockError.unknownLock(peripheral) + } + let lockIdentifier = information.id + guard let lockCache = self[lock: lockIdentifier], + let parentKeyData = self[key: lockCache.key.id] else { + throw LockError.noKey(lock: lockIdentifier) + } + let newKeyIdentifier = UUID() + let parentKey = KeyCredentials( + id: lockCache.key.id, + secret: parentKeyData + ) + let newKey = NewKey( + id: newKeyIdentifier, + name: newKeyName, + permission: permission + ) + let newKeySharedSecret = KeyData() + // file for sharing + let newKeyInvitation = NewKey.Invitation( + lock: lockIdentifier, + key: newKey, + secret: newKeySharedSecret + ) + let newKeyRequest = CreateNewKeyRequest( + key: newKey, + secret: newKeySharedSecret + ) + try await central.createKey( + newKeyRequest, + using: parentKey, + for: peripheral + ) + log("Created new key \(newKey.id) (\(newKey.permission.type))") + return newKeyInvitation + } + + func confirm(_ newKeyInvitation: NewKey.Invitation, name: String) async throws { + guard applicationData.locks[newKeyInvitation.lock] == nil else { + throw LockError.existingKey(lock: newKeyInvitation.lock) + } + guard newKeyInvitation.key.expiration.timeIntervalSinceNow > 0 else { + throw LockError.newKeyExpired + } + let keyData = KeyData() + // recieve new key + let credentials = KeyCredentials( + id: newKeyInvitation.key.id, + secret: newKeyInvitation.secret + ) + guard let (peripheral, information) = lockInformation.first(where: { $0.value.id == newKeyInvitation.lock }) else { + fatalError() // FIXME: + } + // BLE request + try await central.confirmKey( + .init(secret: keyData), + using: credentials, + for: peripheral + ) + // save data + let lockCache = LockCache( + key: Key( + id: newKeyInvitation.key.id, + name: newKeyInvitation.key.name, + created: newKeyInvitation.key.created, + permission: newKeyInvitation.key.permission + ), + name: name, + information: .init(information) + ) + self[lock: newKeyInvitation.lock] = lockCache + self[key: newKeyInvitation.key.id] = keyData + log("Confirmed new key for lock \(information.id)") + } + + @discardableResult + func listKeys( + _ lock: NativeCentral.Peripheral, + notification updateBlock: ((KeysList, Bool) -> ()) = { _,_ in } + ) async throws -> Bool { + + // get lock key + guard let information = self.lockInformation[lock], + let lockCache = self[lock: information.id], + let keyData = self[key: lockCache.key.id] + else { return false } + + let key = KeyCredentials( + id: lockCache.key.id, + secret: keyData + ) + + //let context = backgroundContext + + // BLE request + try await central.connection(for: lock) { + let stream = try await $0.listKeys(using: key, log: { log("📲 Central: " + $0) }) + var list = KeysList() + for try await notification in stream { + list.append(notification.key) + // call completion block + updateBlock(list, notification.isLast)/* + await context.commit { (context) in + try context.insert(notification.key, for: information.id) + }*/ + } + } + + // upload keys to cloud + #if os(iOS) + //updateCloud() + #endif + + return true + } + + @discardableResult + func listEvents( + _ lock: NativeCentral.Peripheral, + fetchRequest: LockEvent.FetchRequest? = nil, + notification updateBlock: @escaping ((EventsList, Bool) -> ()) = { _,_ in } + ) async throws -> Bool { + + // get lock key + guard let information = self.lockInformation[lock], + let lockCache = self[lock: information.id], + let keyData = self[key: lockCache.key.id] + else { return false } + + let key = KeyCredentials( + id: lockCache.key.id, + secret: keyData + ) + + let lockIdentifier = information.id + //let context = backgroundContext + + // BLE request + let log = central.log + try await central.connection(for: lock) { + let stream = try await $0.listEvents(fetchRequest: fetchRequest, using: key, log: log) + var events = [LockEvent]() + for try await notification in stream { + if let event = notification.event { + events.append(event) + log?("Recieved event \(event.id)")/* + await context.commit { (context) in + try context.insert(event, for: information.id) + }*/ + } + // call completion block + updateBlock(events, notification.isLast) + + } + } + + /* + #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: lockIdentifier) + try self?.cloud.upload(value) + } + } catch { + log("⚠️ Could not upload latest events to iCloud: \(error.localizedDescription)") + } + } + } + #endif + */ + return true } } From d9baf3e1ee7de5db92a533324118253034fc20a9 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 10:36:13 -0700 Subject: [PATCH 072/229] [App] Added `LockDetailView` --- Xcode/LockKit/View/LockDetailView.swift | 125 +++++++++++++++++++ Xcode/SmartLock.xcodeproj/project.pbxproj | 4 + Xcode/SmartLock/View/NearbyDevicesView.swift | 10 +- Xcode/SmartLock/View/SidebarView.swift | 60 +++------ 4 files changed, 156 insertions(+), 43 deletions(-) create mode 100644 Xcode/LockKit/View/LockDetailView.swift diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift new file mode 100644 index 00000000..1fffb8dc --- /dev/null +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -0,0 +1,125 @@ +// +// LockDetailView.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/19/22. +// + +import SwiftUI +import CoreLock + +public struct LockDetailView: View { + + @EnvironmentObject + public var store: Store + + public let id: UUID + + public var body: some View { + if let cache = self.cache { + AnyView( + StateView( + id: id, + cache: cache, + unlock: unlock + ) + ) + } else { + AnyView(UnknownView(id: id, information: information)) + } + } + + public init(id: UUID) { + self.id = id + } +} + +private extension LockDetailView { + + var cache: LockCache? { + store[lock: id] + } + + var information: LockInformation? { + store.lockInformation.first(where: { $0.value.id == id })?.value + } + + func unlock() { + Task { + //store.unlock(for:action:) + } + } +} + +extension LockDetailView { + + struct StateView: View { + + let id: UUID + + let cache: LockCache + + let unlock: () -> () + + public var body: some View { + List { + VStack { + // unlock button + Button(action: unlock, label: { + PermissionIconView(permission: cache.key.permission.type) + .frame(width: 150, height: 150, alignment: .center) + }) + // info + Text(verbatim: id.description) + } + } + .navigationTitle(Text(verbatim: cache.name)) + } + } +} + +extension LockDetailView { + + struct UnknownView: View { + + let id: UUID + + let information: LockInformation? + + var body: some View { + VStack { + Text(verbatim: id.description) + } + } + } +} + +// MARK: - Preview + +struct LockDetailView_Previews: PreviewProvider { + static var previews: some View { + Group { + NavigationView { + LockDetailView.StateView( + id: UUID(), + cache: LockCache( + key: Key( + id: UUID(), + name: "Key 1", + created: Date(), + permission: .admin + ), + name: "Home", + information: LockCache.Information( + buildVersion: .current, + version: .current, + status: .unlock, + unlockActions: [.default] + ) + ), + unlock: { } + ) + } + } + } +} diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 045a32d3..64dbeeee 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 6E21831D28D834D200A622B3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21831C28D834D200A622B3 /* ContentView.swift */; }; 6E21831F28D84A7300A622B3 /* SidebarLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21831E28D84A7300A622B3 /* SidebarLabel.swift */; }; 6E21832128D8501500A622B3 /* ProgressIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21832028D8501500A622B3 /* ProgressIndicatorView.swift */; }; + 6E21832328D8D26300A622B3 /* LockDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21832228D8D26300A622B3 /* LockDetailView.swift */; }; 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276BB28D708A000AF171B /* CoreLock */; }; 6E3276C328D70A3700AF171B /* HomeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E3276C228D70A3700AF171B /* HomeKit.framework */; }; 6E3276C628D70A3700AF171B /* RequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276C528D70A3700AF171B /* RequestHandler.swift */; }; @@ -92,6 +93,7 @@ 6E21831C28D834D200A622B3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 6E21831E28D84A7300A622B3 /* SidebarLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarLabel.swift; sourceTree = ""; }; 6E21832028D8501500A622B3 /* ProgressIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressIndicatorView.swift; sourceTree = ""; }; + 6E21832228D8D26300A622B3 /* LockDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockDetailView.swift; sourceTree = ""; }; 6E3276BA28D7088D00AF171B /* SmartLock */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SmartLock; path = ..; sourceTree = ""; }; 6E3276C128D70A3700AF171B /* MatterLock.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MatterLock.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6E3276C228D70A3700AF171B /* HomeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HomeKit.framework; path = Library/Frameworks/HomeKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -211,6 +213,7 @@ 6E4CB61C28D788B900116573 /* UIKit */, 6E3276E528D782B900AF171B /* LockRowView.swift */, 6E4CB61928D788AA00116573 /* PermissionIconView.swift */, + 6E21832228D8D26300A622B3 /* LockDetailView.swift */, ); path = View; sourceTree = ""; @@ -473,6 +476,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6E21832328D8D26300A622B3 /* LockDetailView.swift in Sources */, 6E4CB61F28D7902600116573 /* NSStyleKit.swift in Sources */, 6E21831928D8116C00A622B3 /* NewKeyDocument.swift in Sources */, 6ED248CC28D7A3BF00F78D07 /* ActivityIndicatorView.swift in Sources */, diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index 484cb70a..f9b79f3b 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -14,12 +14,16 @@ struct NearbyDevicesView: View { var store: Store var body: some View { - StateView( + StateView( state: state, items: items, toggleScan: toggleScan, - destination: { - Text(verbatim: "\($0)") + destination: { (item) in + if let information = store.lockInformation.first(where: { $0.key.id == item.id })?.value { + return AnyView(LockDetailView(id: information.id)) + } else { + return AnyView(EmptyView()) + } } ) .onAppear { diff --git a/Xcode/SmartLock/View/SidebarView.swift b/Xcode/SmartLock/View/SidebarView.swift index 11674f3e..a33d5ba2 100644 --- a/Xcode/SmartLock/View/SidebarView.swift +++ b/Xcode/SmartLock/View/SidebarView.swift @@ -36,7 +36,7 @@ struct SidebarView: View { ), isKeysExpanded: $isKeysExpanded ) - DetailView(state: detail) + detail } } } @@ -54,9 +54,8 @@ private extension SidebarView { var keys: [Item] { store.applicationData.locks.values .lazy - .map { $0.key } - .sorted(by: { $0.created < $1.created }) - .map { .key($0.id, $0.name, $0.permission.type) } + .sorted(by: { $0.key.created < $1.key.created }) + .map { .key($0.key.id, $0.name, $0.key.permission.type) } } func toggleNearbyExpanded(_ newValue: Bool) { @@ -95,57 +94,38 @@ private extension SidebarView { } } - var detail: DetailView.State { + var detail: some View { guard let selection = self.selection, let item = locks.first(where: { $0.id == selection }) ?? keys.first(where: { $0.id == selection }) else { - return .empty + return AnyView( + Text("Select a lock") + ) } switch item { case let .lock(id, _, _): // FIXME: store peripheral instead of id guard let peripheral = store.peripherals.keys.first(where: { $0.id == id }) else { - return .empty - } - return .lock(peripheral) - case let .key(id, _, _): - return .key(id) - } - } -} - -extension SidebarView { - - struct DetailView: View { - - let state: State - - var body: some View { - switch state { - case .empty: - AnyView( + return AnyView( Text("Select a lock") ) - case let .lock(peripheral): - AnyView( - Text(verbatim: "Lock \(peripheral)") + } + guard let information = store.lockInformation[peripheral] else { + return AnyView( + ProgressView() + .progressViewStyle(.circular) ) - case let .key(id): - AnyView( - Text(verbatim: "Key \(id)") + } + return AnyView(LockDetailView(id: information.id)) + case let .key(id, _, _): + guard let lock = store.applicationData.locks.first(where: { $0.value.key.id == id })?.key else { + return AnyView( + Text("Select a lock") ) } + return AnyView(LockDetailView(id: lock)) } } } -extension SidebarView.DetailView { - - enum State { - case empty - case lock(NativePeripheral) - case key(UUID) - } -} - extension SidebarView { struct NavigationView: View { From d50d3db0df52cf4cd3b0f9bd0d390f6f0d779b2d Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 11:00:56 -0700 Subject: [PATCH 073/229] [App] Updated `LockDetailView` --- Xcode/LockKit/View/LockDetailView.swift | 72 +++++++++++++++++++++---- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 1fffb8dc..71b3bbdd 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -61,17 +61,66 @@ extension LockDetailView { let unlock: () -> () + private static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .short + return formatter + }() + public var body: some View { - List { - VStack { - // unlock button - Button(action: unlock, label: { - PermissionIconView(permission: cache.key.permission.type) - .frame(width: 150, height: 150, alignment: .center) - }) - // info - Text(verbatim: id.description) + ScrollView { + VStack(alignment: .leading, spacing: 20) { + HStack { + Spacer() + // unlock button + Button(action: unlock, label: { + PermissionIconView(permission: cache.key.permission.type) + .frame(width: 150, height: 150, alignment: .center) + }) + .padding(30) + Spacer() + } + VStack(alignment: .leading, spacing: 20) { + // info + HStack { + Text("Lock") + .frame(width: 100, height: nil, alignment: .leading) + .font(.body) + .foregroundColor(.gray) + Text(verbatim: id.description) + } + HStack { + Text("Key") + .frame(width: 100, height: nil, alignment: .leading) + .font(.body) + .foregroundColor(.gray) + Text(verbatim: cache.key.id.description) + } + HStack { + Text("Type") + .frame(width: 100, height: nil, alignment: .leading) + .font(.body) + .foregroundColor(.gray) + Text(verbatim: cache.key.permission.localizedText) + } + HStack { + Text("Created") + .frame(width: 100, height: nil, alignment: .leading) + .font(.body) + .foregroundColor(.gray) + Text(verbatim: Self.dateFormatter.string(from: cache.key.created)) + } + HStack { + Text("Version") + .frame(width: 100, height: nil, alignment: .leading) + .font(.body) + .foregroundColor(.gray) + Text(verbatim: "v" + cache.information.version.description) + } + } } + .padding(20) } .navigationTitle(Text(verbatim: cache.name)) } @@ -120,6 +169,11 @@ struct LockDetailView_Previews: PreviewProvider { unlock: { } ) } + + NavigationView { + LockDetailView.UnknownView(id: UUID(), information: nil) + } + .previewDisplayName("Unknown View") } } } From 5f6b2be25373f3da623019e621046d490466c752 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 11:25:03 -0700 Subject: [PATCH 074/229] [App] Added event and keys to Lock view --- Xcode/LockKit/View/LockDetailView.swift | 89 ++++++++++++++++++++----- 1 file changed, 72 insertions(+), 17 deletions(-) diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 71b3bbdd..4384f869 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -21,6 +21,8 @@ public struct LockDetailView: View { StateView( id: id, cache: cache, + events: events, + keys: keys, unlock: unlock ) ) @@ -49,6 +51,14 @@ private extension LockDetailView { //store.unlock(for:action:) } } + + var events: Int { + 2 + } + + var keys: Int { + 1 + } } extension LockDetailView { @@ -59,6 +69,10 @@ extension LockDetailView { let cache: LockCache + let events: Int + + let keys: Int + let unlock: () -> () private static let dateFormatter: DateFormatter = { @@ -68,6 +82,11 @@ extension LockDetailView { return formatter }() + + private var titleWidth: CGFloat { + 110 + } + public var body: some View { ScrollView { VStack(alignment: .leading, spacing: 20) { @@ -83,40 +102,74 @@ extension LockDetailView { } VStack(alignment: .leading, spacing: 20) { // info - HStack { - Text("Lock") - .frame(width: 100, height: nil, alignment: .leading) - .font(.body) - .foregroundColor(.gray) - Text(verbatim: id.description) - } - HStack { - Text("Key") - .frame(width: 100, height: nil, alignment: .leading) - .font(.body) - .foregroundColor(.gray) - Text(verbatim: cache.key.id.description) + if cache.key.permission.isAdministrator { + HStack { + Text("Lock") + .frame(width: titleWidth, height: nil, alignment: .leading) + .font(.body) + .foregroundColor(.gray) + Text(verbatim: id.description) + } + HStack { + Text("Key") + .frame(width: titleWidth, height: nil, alignment: .leading) + .font(.body) + .foregroundColor(.gray) + Text(verbatim: cache.key.id.description) + } } HStack { Text("Type") - .frame(width: 100, height: nil, alignment: .leading) + .frame(width: titleWidth, height: nil, alignment: .leading) .font(.body) .foregroundColor(.gray) Text(verbatim: cache.key.permission.localizedText) } HStack { Text("Created") - .frame(width: 100, height: nil, alignment: .leading) + .frame(width: titleWidth, height: nil, alignment: .leading) .font(.body) .foregroundColor(.gray) Text(verbatim: Self.dateFormatter.string(from: cache.key.created)) } HStack { Text("Version") - .frame(width: 100, height: nil, alignment: .leading) + .frame(width: titleWidth, height: nil, alignment: .leading) .font(.body) .foregroundColor(.gray) - Text(verbatim: "v" + cache.information.version.description) + Text(verbatim: "v\(cache.information.version.description)") + } + HStack { + Text("History") + .frame(width: titleWidth, height: nil, alignment: .leading) + .font(.body) + .foregroundColor(.gray) + NavigationLink(destination: { + + }, label: { + HStack { + Text("\(events) events") + Image(systemName: "chevron.right") + } + }) + .foregroundColor(.primary) + } + if cache.key.permission.isAdministrator { + HStack { + Text("Permissions") + .frame(width: 100, height: nil, alignment: .leading) + .font(.body) + .foregroundColor(.gray) + NavigationLink(destination: { + + }, label: { + HStack { + Text("\(keys) keys") + Image(systemName: "chevron.right") + } + }) + .foregroundColor(.primary) + } } } } @@ -166,6 +219,8 @@ struct LockDetailView_Previews: PreviewProvider { unlockActions: [.default] ) ), + events: 10, + keys: 2, unlock: { } ) } From 29eb7b3bb62c9d8164043ccc797047e333f7697b Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 11:57:49 -0700 Subject: [PATCH 075/229] [App] Updated Tab Bar icons --- Xcode/LockKit/View/LockDetailView.swift | 29 +++++++++++++----- .../Tab Bar Icons/Contents.json | 6 ---- .../LockTabBarIcon.imageset/Contents.json | 15 --------- .../lockTabBarIcon.pdf | Bin 2697 -> 0 bytes .../Contents.json | 15 --------- .../lockTabBarIconSelected.pdf | Bin 2646 -> 0 bytes .../NearTabBarIcon.imageset/Contents.json | 26 ---------------- .../NearTabBarIcon.imageset/Near.png | Bin 593 -> 0 bytes .../NearTabBarIcon.imageset/Near@2x.png | Bin 1367 -> 0 bytes .../NearTabBarIcon.imageset/Near@3x.png | Bin 2332 -> 0 bytes .../Contents.json | 26 ---------------- .../NearSelected.png | Bin 534 -> 0 bytes .../NearSelected@2x.png | Bin 1094 -> 0 bytes .../NearSelected@3x.png | Bin 1715 -> 0 bytes .../SettingsTabBarIcon.imageset/Contents.json | 15 --------- .../SettingsTabBarIcon.imageset/Settings.pdf | Bin 5388 -> 0 bytes .../Contents.json | 15 --------- .../SettingsTabBarIconSelected.pdf | Bin 4412 -> 0 bytes Xcode/SmartLock/View/SettingsView.swift | 3 -- Xcode/SmartLock/View/TabBarView.swift | 24 +++++++++++++-- 20 files changed, 43 insertions(+), 131 deletions(-) delete mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/Contents.json delete mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIcon.imageset/Contents.json delete mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIcon.imageset/lockTabBarIcon.pdf delete mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIconSelected.imageset/Contents.json delete mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIconSelected.imageset/lockTabBarIconSelected.pdf delete mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Contents.json delete mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Near.png delete mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Near@2x.png delete mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Near@3x.png delete mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/Contents.json delete mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/NearSelected.png delete mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/NearSelected@2x.png delete mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/NearSelected@3x.png delete mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/SettingsTabBarIcon.imageset/Contents.json delete mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/SettingsTabBarIcon.imageset/Settings.pdf delete mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/SettingsTabBarIconSelected.imageset/Contents.json delete mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/SettingsTabBarIconSelected.imageset/SettingsTabBarIconSelected.pdf diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 4384f869..37c887c3 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -26,6 +26,9 @@ public struct LockDetailView: View { unlock: unlock ) ) + } else if let information = self.information, + information.status == .setup { + AnyView(Text("Setup")) } else { AnyView(UnknownView(id: id, information: information)) } @@ -73,6 +76,9 @@ extension LockDetailView { let keys: Int + @State + var showID = false + let unlock: () -> () private static let dateFormatter: DateFormatter = { @@ -82,9 +88,8 @@ extension LockDetailView { return formatter }() - private var titleWidth: CGFloat { - 110 + 100 } public var body: some View { @@ -102,7 +107,7 @@ extension LockDetailView { } VStack(alignment: .leading, spacing: 20) { // info - if cache.key.permission.isAdministrator { + if showID { HStack { Text("Lock") .frame(width: titleWidth, height: nil, alignment: .leading) @@ -123,7 +128,12 @@ extension LockDetailView { .frame(width: titleWidth, height: nil, alignment: .leading) .font(.body) .foregroundColor(.gray) - Text(verbatim: cache.key.permission.localizedText) + Button(action: { + showID.toggle() + }, label: { + Text(verbatim: cache.key.permission.localizedText) + .foregroundColor(.primary) + }) } HStack { Text("Created") @@ -145,7 +155,7 @@ extension LockDetailView { .font(.body) .foregroundColor(.gray) NavigationLink(destination: { - + Text("Events") }, label: { HStack { Text("\(events) events") @@ -157,11 +167,11 @@ extension LockDetailView { if cache.key.permission.isAdministrator { HStack { Text("Permissions") - .frame(width: 100, height: nil, alignment: .leading) + .frame(width: titleWidth, height: nil, alignment: .leading) .font(.body) .foregroundColor(.gray) NavigationLink(destination: { - + Text("Keys") }, label: { HStack { Text("\(keys) keys") @@ -226,7 +236,10 @@ struct LockDetailView_Previews: PreviewProvider { } NavigationView { - LockDetailView.UnknownView(id: UUID(), information: nil) + LockDetailView.UnknownView( + id: UUID(), + information: LockInformation(id: UUID(), status: .unlock) + ) } .previewDisplayName("Unknown View") } diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/Contents.json b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/Contents.json deleted file mode 100644 index 73c00596..00000000 --- a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIcon.imageset/Contents.json b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIcon.imageset/Contents.json deleted file mode 100644 index ea94054b..00000000 --- a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIcon.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "lockTabBarIcon.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIcon.imageset/lockTabBarIcon.pdf b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIcon.imageset/lockTabBarIcon.pdf deleted file mode 100644 index aeb426b239c63ce7382efdee8be0618760990a4f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2697 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 K3T3^&C;5Ns{Ncg? diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Contents.json b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Contents.json deleted file mode 100644 index 59bce20c..00000000 --- a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Contents.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "images" : [ - { - "filename" : "Near.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "Near@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "Near@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Near.png b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Near.png deleted file mode 100644 index ee9521e5ac75cb374c760df1329ce140ffff6045..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 593 zcmV-X0P000>X1^@s6#OZ}&00001b5ch_0Itp) z=>Px%3rR#lR7efImd#5QQ5c7>Auhr~`@ql=B*6}mxh0|qLh*_}3qp1}wB2TqihKKkjKXoA)|0}DgyBKQJkpuZ~NHT{#Sm_GXHTaG8`qq73ldPKmLPr<$FLHesu z4#W-pgb^F@B-d#l8i@3OtE=|xcf()U2e;)`bmemXmLJ;)NI!j(Nsef@8;In$X#w6q zdMi5Ue}TKO2}_VA{q#*H*`n>%#BvqsJB#B^7(X``aS#s#uKMYloQhAhpFq9S<#xHn zZrvheNgw_6O;q$f@p*WMuae4eBlw<}ItTUM_&enIEFMChf^WP9MZR74$#XGz%Kdg( z=+|=>hU^67iSbBh$vW%x$(1YF2H%lJWPU?+V-z&)moe|iHtjb)Z<2VcoXou*gdXsK fH1yvJ`B$$qXI*N-GvpM400000NkvXXu0mjfJwF?7 diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Near@2x.png b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Near@2x.png deleted file mode 100644 index f23864f0ff7fcc1331ef10216d1f50c4c6e96de1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1367 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 YFT6D`gbUVa82|tP07*qoM6N<$f*+vs=Kufz diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/NearSelected@2x.png b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/NearSelected@2x.png deleted file mode 100644 index 9305aeb1540269c1fa3fe6ea2cef30d44ee8f7a0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1094 zcmV-M1iAZ(P)Px(07*naRA>e5n$1fVK^(_bieA8|pu!Nr0_jkMcrAKv9lAx*rEb+B>J;Ac;Gus& z5Gp$<1VMEWf}l+@lg)t@{aXH9E5*i z9zkVnl}kC5+Y1!vDl8+ajIVN+9?WWj4A*fG;RHy7v=qhwo?)4QTLoI7 zk=}&06m76R;K9pxZ1fR=i(YrFjt!(-Jo>a;+Wc4m<+a!Yp)RkmrNU)FjPSw1gGP zvR`RGh;aiR40xULC(Oc03^r#OrSwOdR~ZI7jC!8x7mT$XUV@MEIAW`3rp$Loq}d!u z+Jl&;O$P{i#l8YxrWs|lAEUoL!^YN~sQDfq=KYse*ACPNx7so|^tEF19H!-QX_w|I z%oPdS(#mIaXm=TdpP;tBP0$93`36I{T_67Lv%oX_>Z5qC>R%IQoe+D^ZA7VZ~#q1B5s z^$p_kAZOVucxg0OMBf6pU@f6%qx2b7AW&MRIT89;nTA=j;o5Jmh`vFzb-Vi7?4(gz zBbY0q#;5RZ)a$o$ig4y_RwlYMN~;wWdVL)}Xhk-_E$}V0&9ZBs)8PZTs{6aI;~))0 z-z0s|RcMx-^KlS4tA}8d9+K;s=zwKk4#UAolRm_9p%?2p7zQa)>fP)t)GvGyf73@= zq^S@F@G^ttt58t75-TtHzrT_o{ M07*qoM6N<$f(671&;S4c diff --git a/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/NearSelected@3x.png b/Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/NearSelected@3x.png deleted file mode 100644 index c1163b2d23bad1355a5a4988bd74277d89bf7645..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1715 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 diff --git a/Xcode/SmartLock/View/SettingsView.swift b/Xcode/SmartLock/View/SettingsView.swift index 73f4ddd6..03743e9f 100644 --- a/Xcode/SmartLock/View/SettingsView.swift +++ b/Xcode/SmartLock/View/SettingsView.swift @@ -11,9 +11,6 @@ struct SettingsView: View { var body: some View { Text("Settings") .navigationTitle("Settings") - .tabItem { - Label("Settings", image: "SettingsTabBarIconSelected") - } } } diff --git a/Xcode/SmartLock/View/TabBarView.swift b/Xcode/SmartLock/View/TabBarView.swift index 2a57ce88..c1c2c312 100644 --- a/Xcode/SmartLock/View/TabBarView.swift +++ b/Xcode/SmartLock/View/TabBarView.swift @@ -12,19 +12,39 @@ import LockKit struct TabBarView: View { var body: some View { TabView { + // Nearby NavigationView { NearbyDevicesView() Text("Select a lock") } .tabItem { - Label("Nearby", image: "NearTabBarIconSelected") + Label("Nearby", systemImage: "location.circle.fill") } + + // Keys + NavigationView { + EmptyView() + Text("Select a lock") + } + .tabItem { + Label("Keys", systemImage: "key.fill") + } + + // Keys + NavigationView { + EmptyView() + } + .tabItem { + Label("History", systemImage: "clock.fill") + } + + // Settings NavigationView { SettingsView() Text("Settings detail") } .tabItem { - Label("Settings", image: "SettingsTabBarIconSelected") + Label("Settings", systemImage: "gearshape.fill") } } .navigationViewStyle(.stack) From 5b334def9ee38d220d061d72f164d9afe6475fa6 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 12:22:34 -0700 Subject: [PATCH 076/229] [App] Added CoreData types --- .../ConfirmNewKeyEventManagedObject.swift | 78 +++++++++ .../Model/CoreData/ContactManagedObject.swift | 160 ++++++++++++++++++ .../CreateNewKeyEventManagedObject.swift | 75 ++++++++ .../Model/CoreData/EventManagedObject.swift | 145 ++++++++++++++++ .../Model/CoreData/KeyManagedObject.swift | 97 +++++++++++ .../LockInformationManagedObject.swift | 52 ++++++ .../Model/CoreData/LockManagedObject.swift | 100 +++++++++++ .../Model/CoreData/ManagedObject.swift | 116 +++++++++++++ .../Model.xcdatamodel/contents | 98 +++++++++++ .../Model/CoreData/NewKeyManagedObject.swift | 90 ++++++++++ .../Model/CoreData/PersistentContainer.swift | 64 +++++++ .../RemoveKeyEventManagedObject.swift | 103 +++++++++++ .../CoreData/ScheduleManagedObject.swift | 50 ++++++ .../CoreData/SetupEventManagedObject.swift | 43 +++++ .../CoreData/UnlockEventManagedObject.swift | 44 +++++ Xcode/LockKit/Model/Store.swift | 122 +++++++++++-- Xcode/SmartLock.xcodeproj/project.pbxproj | 87 +++++++++- .../SmartLock.xcdatamodeld/.xccurrentversion | 5 - .../SmartLock.xcdatamodel/contents | 9 - 19 files changed, 1504 insertions(+), 34 deletions(-) create mode 100644 Xcode/LockKit/Model/CoreData/ConfirmNewKeyEventManagedObject.swift create mode 100644 Xcode/LockKit/Model/CoreData/ContactManagedObject.swift create mode 100644 Xcode/LockKit/Model/CoreData/CreateNewKeyEventManagedObject.swift create mode 100644 Xcode/LockKit/Model/CoreData/EventManagedObject.swift create mode 100644 Xcode/LockKit/Model/CoreData/KeyManagedObject.swift create mode 100644 Xcode/LockKit/Model/CoreData/LockInformationManagedObject.swift create mode 100644 Xcode/LockKit/Model/CoreData/LockManagedObject.swift create mode 100644 Xcode/LockKit/Model/CoreData/ManagedObject.swift create mode 100644 Xcode/LockKit/Model/CoreData/Model.xcdatamodeld/Model.xcdatamodel/contents create mode 100644 Xcode/LockKit/Model/CoreData/NewKeyManagedObject.swift create mode 100644 Xcode/LockKit/Model/CoreData/PersistentContainer.swift create mode 100644 Xcode/LockKit/Model/CoreData/RemoveKeyEventManagedObject.swift create mode 100644 Xcode/LockKit/Model/CoreData/ScheduleManagedObject.swift create mode 100644 Xcode/LockKit/Model/CoreData/SetupEventManagedObject.swift create mode 100644 Xcode/LockKit/Model/CoreData/UnlockEventManagedObject.swift delete mode 100644 Xcode/SmartLock/Model/SmartLock.xcdatamodeld/.xccurrentversion delete mode 100644 Xcode/SmartLock/Model/SmartLock.xcdatamodeld/SmartLock.xcdatamodel/contents diff --git a/Xcode/LockKit/Model/CoreData/ConfirmNewKeyEventManagedObject.swift b/Xcode/LockKit/Model/CoreData/ConfirmNewKeyEventManagedObject.swift new file mode 100644 index 00000000..4f97d530 --- /dev/null +++ b/Xcode/LockKit/Model/CoreData/ConfirmNewKeyEventManagedObject.swift @@ -0,0 +1,78 @@ +// +// ConfirmNewKeyEventManagedObject.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/6/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreData +import CoreLock + +public final class ConfirmNewKeyEventManagedObject: EventManagedObject { + + @nonobjc override class var eventType: LockEvent.EventType { return .confirmNewKey } + + internal convenience init(_ value: LockEvent.ConfirmNewKey, + lock: LockManagedObject, + context: NSManagedObjectContext) { + + self.init(context: context) + self.identifier = value.id + self.lock = lock + self.date = value.date + self.key = value.key + self.pendingKey = value.newKey + } +} + +internal extension LockEvent.ConfirmNewKey { + + init?(managedObject: ConfirmNewKeyEventManagedObject) { + + guard let id = managedObject.identifier, + let date = managedObject.date, + let key = managedObject.key, + let pendingKey = managedObject.pendingKey + else { return nil } + + self.init(id: id, date: date, newKey: pendingKey, key: key) + } +} + +// MARK: - IdentifiableManagedObject + +extension ConfirmNewKeyEventManagedObject: IdentifiableManagedObject { } + +// MARK: - Fetch + +public extension ConfirmNewKeyEventManagedObject { + + /// Fetch the new key specified by the event. + func newKey(in context: NSManagedObjectContext) throws -> NewKeyManagedObject? { + + guard let newKey = self.pendingKey else { + assertionFailure("Missing key value") + return nil + } + return try context.find(id: newKey, type: NewKeyManagedObject.self) + } + + /// Fetch the removed key specified by the event. + func createKeyEvent(in context: NSManagedObjectContext) throws -> CreateNewKeyEventManagedObject? { + + guard let newKey = self.pendingKey else { + assertionFailure("Missing new key value") + return nil + } + + let fetchRequest = NSFetchRequest() + fetchRequest.entity = CreateNewKeyEventManagedObject.entity() + fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(CreateNewKeyEventManagedObject.pendingKey), newKey as NSUUID) + fetchRequest.fetchLimit = 1 + fetchRequest.includesSubentities = false + fetchRequest.returnsObjectsAsFaults = false + return try context.fetch(fetchRequest).first + } +} diff --git a/Xcode/LockKit/Model/CoreData/ContactManagedObject.swift b/Xcode/LockKit/Model/CoreData/ContactManagedObject.swift new file mode 100644 index 00000000..cfb1449e --- /dev/null +++ b/Xcode/LockKit/Model/CoreData/ContactManagedObject.swift @@ -0,0 +1,160 @@ +// +// ContactManagedObject.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/22/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreData +import CoreLock +import CloudKit +import Contacts + +public final class ContactManagedObject: NSManagedObject { + + internal convenience init(identifier: String, context: NSManagedObjectContext) { + + self.init(context: context) + self.identifier = identifier + } + + internal static func find(_ identifier: String, in context: NSManagedObjectContext) throws -> ContactManagedObject? { + + try context.find(identifier: identifier as NSString, + propertyName: #keyPath(ContactManagedObject.identifier), + type: ContactManagedObject.self) + } +} + +// MARK: - Computed Properties + +public extension ContactManagedObject { + + var nameComponents: PersonNameComponents? { + get { + var nameComponents = PersonNameComponents() + nameComponents.namePrefix = namePrefix + nameComponents.givenName = givenName + nameComponents.middleName = middleName + nameComponents.familyName = familyName + nameComponents.nameSuffix = nameSuffix + nameComponents.nickname = nickname + return nameComponents + } + set { + assert(newValue?.phoneticRepresentation == nil) + namePrefix = newValue?.namePrefix + givenName = newValue?.givenName + middleName = newValue?.middleName + familyName = newValue?.familyName + nameSuffix = newValue?.nameSuffix + nickname = newValue?.nickname + } + } +} + +// MARK: - Fetch + +public extension ContactManagedObject { + + static func fetch(in context: NSManagedObjectContext) throws -> [ContactManagedObject] { + let fetchRequest = NSFetchRequest() + fetchRequest.entity = entity() + fetchRequest.fetchBatchSize = 40 + fetchRequest.sortDescriptors = [ + .init(keyPath: \ContactManagedObject.identifier, ascending: true) + ] + return try context.fetch(fetchRequest) + } +} + +// MARK: - Store + +public extension Store { + /* + #if os(iOS) + func updateContacts() throws { + + // exclude self + let currentUser = try cloud.container.fetchUserRecordID() + + // insert new contacts + var insertedUsers = Set() + let context = backgroundContext + try cloud.container.discoverAllUserIdentities { (user) in + guard let userRecordID = user.userRecordID, + userRecordID != currentUser + else { return } + insertedUsers.insert(userRecordID.recordName) + context.commit { try $0.insert(contact: user) } + } + + // delete old contacts + let fetchRequest = NSFetchRequest() + fetchRequest.entity = ContactManagedObject.entity() + fetchRequest.sortDescriptors = [ + .init(keyPath: \ContactManagedObject.identifier, ascending: true) + ] + fetchRequest.predicate = NSPredicate(format: "NONE %K IN %@", #keyPath(ContactManagedObject.identifier), insertedUsers) + context.commit { (context) in + try context.fetch(fetchRequest).forEach { + context.delete($0) + } + } + } + #endif + */ +} + +internal extension ContactManagedObject { + + static let contactStore = CNContactStore() +} + +internal extension NSManagedObjectContext { + + @discardableResult + func insert(contact identity: CKUserIdentity) throws -> ContactManagedObject? { + + guard let userRecordID = identity.userRecordID + else { return nil } + + // find or create + let identifier = userRecordID.recordName + let managedObject = try ContactManagedObject.find(identifier, in: self) + ?? ContactManagedObject(identifier: identifier, context: self) + + // update values + managedObject.nameComponents = identity.nameComponents + if let email = identity.lookupInfo?.emailAddress { + managedObject.email = email + } + if let phoneNumber = identity.lookupInfo?.phoneNumber { + managedObject.phone = phoneNumber + } + + // find contact in address book + if CNContactStore.authorizationStatus(for: .contacts) == .authorized { + do { + let contactStore = ContactManagedObject.contactStore + let predicate = CNContact.predicateForContacts(withIdentifiers: identity.contactIdentifiers) + let contacts = try contactStore.unifiedContacts(matching: predicate, keysToFetch: [ + CNContactThumbnailImageDataKey as NSString, + CNContactEmailAddressesKey as NSString, + CNContactPhoneNumbersKey as NSString + ]) + managedObject.email = contacts.compactMap({ $0.emailAddresses.first?.value as String? }).first + managedObject.phone = contacts.compactMap({ $0.phoneNumbers.first?.value.stringValue }).first + managedObject.image = contacts.compactMap({ $0.thumbnailImageData }).first + } catch { + #if DEBUG + log("⚠️ Unable to update contact information from address book. \(error)") + #endif + } + } + + return managedObject + } +} diff --git a/Xcode/LockKit/Model/CoreData/CreateNewKeyEventManagedObject.swift b/Xcode/LockKit/Model/CoreData/CreateNewKeyEventManagedObject.swift new file mode 100644 index 00000000..428d352f --- /dev/null +++ b/Xcode/LockKit/Model/CoreData/CreateNewKeyEventManagedObject.swift @@ -0,0 +1,75 @@ +// +// CreateNewKeyEventManagedObject.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/6/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreData +import CoreLock + +public final class CreateNewKeyEventManagedObject: EventManagedObject { + + @nonobjc override class var eventType: LockEvent.EventType { return .createNewKey } + + internal convenience init(_ value: LockEvent.CreateNewKey, lock: LockManagedObject, context: NSManagedObjectContext) { + + self.init(context: context) + self.identifier = value.id + self.lock = lock + self.date = value.date + self.key = value.key + self.pendingKey = value.newKey + } +} + +public extension LockEvent.CreateNewKey { + + init?(managedObject: CreateNewKeyEventManagedObject) { + + guard let id = managedObject.identifier, + let date = managedObject.date, + let key = managedObject.key, + let pendingKey = managedObject.pendingKey + else { return nil } + + self.init(id: id, date: date, key: key, newKey: pendingKey) + } +} + +// MARK: - IdentifiableManagedObject + +extension CreateNewKeyEventManagedObject: IdentifiableManagedObject { } + +// MARK: - Fetch + +public extension CreateNewKeyEventManagedObject { + + /// Fetch the new key specified by the event. + func newKey(in context: NSManagedObjectContext) throws -> NewKeyManagedObject? { + + guard let newKey = self.pendingKey else { + assertionFailure("Missing key value") + return nil + } + return try context.find(id: newKey, type: NewKeyManagedObject.self) + } + + func confirmKeyEvent(in context: NSManagedObjectContext) throws -> ConfirmNewKeyEventManagedObject? { + + guard let newKey = self.pendingKey else { + assertionFailure("Missing new key value") + return nil + } + + let fetchRequest = NSFetchRequest() + fetchRequest.entity = ConfirmNewKeyEventManagedObject.entity() + fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(ConfirmNewKeyEventManagedObject.pendingKey), newKey as NSUUID) + fetchRequest.fetchLimit = 1 + fetchRequest.includesSubentities = false + fetchRequest.returnsObjectsAsFaults = false + return try context.fetch(fetchRequest).first + } +} diff --git a/Xcode/LockKit/Model/CoreData/EventManagedObject.swift b/Xcode/LockKit/Model/CoreData/EventManagedObject.swift new file mode 100644 index 00000000..bab923e1 --- /dev/null +++ b/Xcode/LockKit/Model/CoreData/EventManagedObject.swift @@ -0,0 +1,145 @@ +// +// EventManagedObject.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/6/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreData +import CoreLock + +public class EventManagedObject: NSManagedObject { + + @nonobjc class var eventType: LockEvent.EventType { fatalError("Implemented by subclass") } + + internal static func initWith(_ value: LockEvent, lock: LockManagedObject, context: NSManagedObjectContext) -> EventManagedObject { + + switch value { + case let .setup(event): + return SetupEventManagedObject(event, lock: lock, context: context) + case let .unlock(event): + return UnlockEventManagedObject(event, lock: lock, context: context) + case let .createNewKey(event): + return CreateNewKeyEventManagedObject(event, lock: lock, context: context) + case let .confirmNewKey(event): + return ConfirmNewKeyEventManagedObject(event, lock: lock, context: context) + case let .removeKey(event): + return RemoveKeyEventManagedObject(event, lock: lock, context: context) + } + } + + internal static func find(_ id: UUID, in context: NSManagedObjectContext) throws -> EventManagedObject? { + + try context.find(identifier: id as NSUUID, + propertyName: #keyPath(EventManagedObject.identifier), + type: EventManagedObject.self) + } +} + +internal extension LockEvent { + + init?(managedObject: EventManagedObject) { + + switch managedObject { + case let eventManagedObject as SetupEventManagedObject: + guard let event = Setup(managedObject: eventManagedObject) + else { return nil } + self = .setup(event) + case let eventManagedObject as UnlockEventManagedObject: + guard let event = Unlock(managedObject: eventManagedObject) + else { return nil } + self = .unlock(event) + case let eventManagedObject as CreateNewKeyEventManagedObject: + guard let event = CreateNewKey(managedObject: eventManagedObject) + else { return nil } + self = .createNewKey(event) + case let eventManagedObject as ConfirmNewKeyEventManagedObject: + guard let event = ConfirmNewKey(managedObject: eventManagedObject) + else { return nil } + self = .confirmNewKey(event) + case let eventManagedObject as RemoveKeyEventManagedObject: + guard let event = RemoveKey(managedObject: eventManagedObject) + else { return nil } + self = .removeKey(event) + default: + assertionFailure("Invalid \(managedObject)") + return nil + } + } +} + +// MARK: - Fetch + +public extension EventManagedObject { + + /// Fetch the key specified by the event. + func key(in context: NSManagedObjectContext) throws -> KeyManagedObject? { + + guard let key = self.key else { + assertionFailure("Missing key value") + return nil + } + + return try context.find(id: key, type: KeyManagedObject.self) + } +} + +public extension LockManagedObject { + + func lastEvent(in context: NSManagedObjectContext) throws -> EventManagedObject? { + + let fetchRequest = NSFetchRequest() + fetchRequest.entity = EventManagedObject.entity() + fetchRequest.fetchBatchSize = 10 + fetchRequest.includesSubentities = true + fetchRequest.shouldRefreshRefetchedObjects = false + fetchRequest.returnsObjectsAsFaults = true + fetchRequest.includesPropertyValues = false + fetchRequest.sortDescriptors = [ + NSSortDescriptor( + key: #keyPath(EventManagedObject.date), + ascending: false + ) + ] + fetchRequest.predicate = NSPredicate( + format: "%K == %@", + #keyPath(EventManagedObject.lock), + self + ) + return try context.fetch(fetchRequest).first + } +} + +// MARK: - Store + +internal extension NSManagedObjectContext { + + @discardableResult + func insert(_ events: [LockEvent], for lock: LockManagedObject) throws -> [EventManagedObject] { + return try events.map { + try insert($0, for: lock) + } + } + + @discardableResult + func insert(_ event: LockEvent, for lock: LockManagedObject) throws -> EventManagedObject { + try EventManagedObject.find(event.id, in: self) + ?? EventManagedObject.initWith(event, lock: lock, context: self) + } + + @discardableResult + func insert(_ events: [LockEvent], for lock: UUID) throws -> [EventManagedObject] { + let managedObject = try find(id: lock, type: LockManagedObject.self) + ?? LockManagedObject(id: lock, name: "", context: self) + return try insert(events, for: managedObject) + } + + @discardableResult + func insert(_ event: LockEvent, for lock: UUID) throws -> EventManagedObject { + let managedObject = try find(id: lock, type: LockManagedObject.self) + ?? LockManagedObject(id: lock, name: "", context: self) + return try insert(event, for: managedObject) + } +} diff --git a/Xcode/LockKit/Model/CoreData/KeyManagedObject.swift b/Xcode/LockKit/Model/CoreData/KeyManagedObject.swift new file mode 100644 index 00000000..55121b3b --- /dev/null +++ b/Xcode/LockKit/Model/CoreData/KeyManagedObject.swift @@ -0,0 +1,97 @@ +// +// KeyManagedObject.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/6/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreData +import CoreLock + +public final class KeyManagedObject: NSManagedObject { + + internal convenience init(_ value: Key, lock: LockManagedObject, context: NSManagedObjectContext) { + self.init(context: context) + self.identifier = value.id + self.lock = lock + self.name = value.name + self.created = value.created + self.permission = numericCast(value.permission.type.rawValue) + if case let .scheduled(schedule) = value.permission { + self.schedule = .init(schedule, context: context) + } + } +} + +public extension Key { + + init?(managedObject: KeyManagedObject) { + + guard let id = managedObject.identifier, + let name = managedObject.name, + let created = managedObject.created, + let permissionType = PermissionType(rawValue: numericCast(managedObject.permission)) + else { return nil } + + let permission: Permission + switch permissionType { + case .owner: + permission = .owner + case .admin: + permission = .owner + case .anytime: + permission = .anytime + case .scheduled: + guard let schedule = managedObject.schedule.flatMap({ Permission.Schedule(managedObject: $0) }) + else { return nil } + permission = .scheduled(schedule) + } + + self.init( + id: id, + name: name, + created: created, + permission: permission + ) + } +} + +// MARK: - IdentifiableManagedObject + +extension KeyManagedObject: IdentifiableManagedObject { } + +// MARK: - Store + +internal extension NSManagedObjectContext { + + @discardableResult + func insert(_ key: Key, for lock: LockManagedObject) throws -> KeyManagedObject { + + if let managedObject = try find(id: key.id, type: KeyManagedObject.self) { + assert(managedObject.lock == lock, "Key stored with conflicting lock") + return managedObject + } else { + return KeyManagedObject(key, lock: lock, context: self) + } + } + + @discardableResult + func insert(_ key: Key, for lock: UUID) throws -> KeyManagedObject { + + let managedObject = try find(id: lock, type: LockManagedObject.self) + ?? LockManagedObject(id: lock, name: "", context: self) + return try insert(key, for: managedObject) + } + + @discardableResult + func insert(_ key: KeyListNotification.KeyValue, for lock: UUID) throws -> NSManagedObject { + switch key { + case let .key(key): + return try insert(key, for: lock) + case let .newKey(key): + return try insert(key, for: lock) + } + } +} diff --git a/Xcode/LockKit/Model/CoreData/LockInformationManagedObject.swift b/Xcode/LockKit/Model/CoreData/LockInformationManagedObject.swift new file mode 100644 index 00000000..fb81b2b4 --- /dev/null +++ b/Xcode/LockKit/Model/CoreData/LockInformationManagedObject.swift @@ -0,0 +1,52 @@ +// +// LockInformationManagedObject.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/6/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreData +import CoreLock + +public final class LockInformationManagedObject: NSManagedObject { + + internal convenience init(_ value: LockCache.Information, context: NSManagedObjectContext) { + self.init(context: context) + update(value) + } + + internal func update(_ value: LockCache.Information) { + + self.buildVersion = numericCast(value.buildVersion.rawValue) + self.versionMajor = numericCast(value.version.major) + self.versionMinor = numericCast(value.version.minor) + self.versionPatch = numericCast(value.version.patch) + self.status = numericCast(value.status.rawValue) + self.defaultUnlockAction = value.unlockActions.contains(.default) + self.buttonUnlockAction = value.unlockActions.contains(.button) + } +} + +internal extension LockCache.Information { + + init?(managedObject: LockInformationManagedObject) { + guard let status = LockStatus(rawValue: numericCast(managedObject.status)) + else { return nil } + self.status = status + self.buildVersion = LockBuildVersion(rawValue: numericCast(managedObject.buildVersion)) + self.version = LockVersion( + major: numericCast(managedObject.versionMajor), + minor: numericCast(managedObject.versionMinor), + patch: numericCast(managedObject.versionPatch) + ) + self.unlockActions = [] + if managedObject.defaultUnlockAction { + self.unlockActions.insert(.default) + } + if managedObject.buttonUnlockAction { + self.unlockActions.insert(.button) + } + } +} diff --git a/Xcode/LockKit/Model/CoreData/LockManagedObject.swift b/Xcode/LockKit/Model/CoreData/LockManagedObject.swift new file mode 100644 index 00000000..bc92f086 --- /dev/null +++ b/Xcode/LockKit/Model/CoreData/LockManagedObject.swift @@ -0,0 +1,100 @@ +// +// LockManagedObject.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/6/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreData +import CoreLock + +public final class LockManagedObject: NSManagedObject { + + internal convenience init(id: UUID, + name: String, + information: LockCache.Information? = nil, + context: NSManagedObjectContext) { + + self.init(context: context) + self.identifier = id + self.name = name + if let information = information { + update(information: information, context: context) + } + } +} + +internal extension LockManagedObject { + + func update(information: LockCache.Information, context: NSManagedObjectContext) { + + if let managedObject = self.information { + managedObject.update(information) + } else { + self.information = LockInformationManagedObject(information, context: context) + } + } +} + +// MARK: - IdentifiableManagedObject + +extension LockManagedObject: IdentifiableManagedObject { } + +// MARK: - Fetch + +public extension LockManagedObject { + + static func fetch(in context: NSManagedObjectContext, sort: [NSSortDescriptor] = []) throws -> [LockManagedObject] { + let fetchRequest = NSFetchRequest() + fetchRequest.entity = entity() + fetchRequest.fetchBatchSize = 10 + fetchRequest.sortDescriptors = sort.isEmpty == false ? sort : [ + .init(keyPath: \LockManagedObject.identifier, ascending: true) + ] + return try context.fetch(fetchRequest) + } +} + +// MARK: - Store + +internal extension NSManagedObjectContext { + + @discardableResult + func insert(_ locks: [UUID: LockCache]) throws -> [LockManagedObject] { + + // insert locks + return try locks.map { (identifier, cache) in + if let managedObject = try find(id: identifier, type: LockManagedObject.self) { + managedObject.name = cache.name + managedObject.update(information: cache.information, context: self) + return managedObject + } else { + return LockManagedObject(id: identifier, + name: cache.name, + information: cache.information, + context: self) + } + } + } + /* + #if os(iOS) + @discardableResult + func insert(_ cloudValue: CloudLock) throws -> LockManagedObject { + + // insert lock + let lockManagedObject: LockManagedObject + if let managedObject = try find(id: cloudValue.id.rawValue, type: LockManagedObject.self) { + managedObject.name = cloudValue.name + lockManagedObject = managedObject + } else { + lockManagedObject = LockManagedObject(id: cloudValue.id.rawValue, + name: cloudValue.name, + context: self) + } + return lockManagedObject + } + #endif + */ +} diff --git a/Xcode/LockKit/Model/CoreData/ManagedObject.swift b/Xcode/LockKit/Model/CoreData/ManagedObject.swift new file mode 100644 index 00000000..c22dc95b --- /dev/null +++ b/Xcode/LockKit/Model/CoreData/ManagedObject.swift @@ -0,0 +1,116 @@ +// +// ManagedObject.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/5/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreData + +public extension NSManagedObjectModel { + + static var lock: NSManagedObjectModel { + guard let url = Bundle(for: Store.self).url(forResource: "Model", withExtension: "momd") + else { fatalError("No url for model") } + guard let model = NSManagedObjectModel(contentsOf: url) + else { fatalError("No model at \(url.path)") } + return model + } +} + +internal extension NSManagedObjectContext { + + /// Wraps the block to allow for error throwing. + func performErrorBlockAndWait(_ block: @escaping () throws -> (T)) throws -> T { + + var blockError: Swift.Error? + var value: T! + performAndWait { + do { value = try block() } + catch { blockError = error } + return + } + + if let error = blockError { + throw error + } + return value + } + + func commit(_ block: @escaping (NSManagedObjectContext) throws -> ()) { + + assert(concurrencyType == .privateQueueConcurrencyType) + perform { [unowned self] in + self.reset() + do { + try block(self) + if self.hasChanges { + try self.save() + } + } catch { + log("⚠️ Unable to commit changes: \(error.localizedDescription)") + #if DEBUG + print(error) + #endif + assertionFailure("Core Data error") + return + } + } + } + + func commit(_ block: @escaping (NSManagedObjectContext) throws -> ()) async { + + assert(concurrencyType == .privateQueueConcurrencyType) + await perform { [unowned self] in + self.reset() + do { + try block(self) + if self.hasChanges { + try self.save() + } + } catch { + log("⚠️ Unable to commit changes: \(error.localizedDescription)") + #if DEBUG + print(error) + #endif + assertionFailure("Core Data error") + return + } + } + } +} + +internal extension NSManagedObjectContext { + + func find(identifier: NSObject, propertyName: String, type: T.Type) throws -> T? where T: NSManagedObject { + + let fetchRequest = NSFetchRequest() + fetchRequest.entity = T.entity() + fetchRequest.predicate = NSPredicate(format: "%K == %@", propertyName, identifier) + fetchRequest.fetchLimit = 1 + fetchRequest.includesSubentities = true + fetchRequest.returnsObjectsAsFaults = false + return try self.fetch(fetchRequest).first + } +} + +public protocol IdentifiableManagedObject { + + var identifier: UUID? { get } +} + +public extension NSManagedObjectContext { + + func find(id: UUID, type: T.Type) throws -> T? where T: IdentifiableManagedObject, T: NSManagedObject { + + let fetchRequest = NSFetchRequest() + fetchRequest.entity = T.entity() + fetchRequest.predicate = NSPredicate(format: "%K == %@", "identifier", id as NSUUID) + fetchRequest.fetchLimit = 1 + fetchRequest.includesSubentities = true + fetchRequest.returnsObjectsAsFaults = false + return try self.fetch(fetchRequest).first + } +} diff --git a/Xcode/LockKit/Model/CoreData/Model.xcdatamodeld/Model.xcdatamodel/contents b/Xcode/LockKit/Model/CoreData/Model.xcdatamodeld/Model.xcdatamodel/contents new file mode 100644 index 00000000..5f98f1b8 --- /dev/null +++ b/Xcode/LockKit/Model/CoreData/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Xcode/LockKit/Model/CoreData/NewKeyManagedObject.swift b/Xcode/LockKit/Model/CoreData/NewKeyManagedObject.swift new file mode 100644 index 00000000..5d58ee62 --- /dev/null +++ b/Xcode/LockKit/Model/CoreData/NewKeyManagedObject.swift @@ -0,0 +1,90 @@ +// +// NewKeyManagedObject.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/8/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreData +import CoreLock + +public final class NewKeyManagedObject: NSManagedObject { + + internal convenience init(_ value: NewKey, lock: LockManagedObject, context: NSManagedObjectContext) { + self.init(context: context) + self.identifier = value.id + self.lock = lock + self.name = value.name + self.created = value.created + self.permission = numericCast(value.permission.type.rawValue) + if case let .scheduled(schedule) = value.permission { + self.schedule = .init(schedule, context: context) + } + self.expiration = value.expiration + } +} + +public extension NewKey { + + init?(managedObject: NewKeyManagedObject) { + + guard let id = managedObject.identifier, + let name = managedObject.name, + let created = managedObject.created, + let permissionType = PermissionType(rawValue: numericCast(managedObject.permission)), + let expiration = managedObject.expiration + else { return nil } + + let permission: Permission + switch permissionType { + case .owner: + permission = .owner + case .admin: + permission = .owner + case .anytime: + permission = .anytime + case .scheduled: + guard let schedule = managedObject.schedule.flatMap({ Permission.Schedule(managedObject: $0) }) + else { return nil } + permission = .scheduled(schedule) + } + + self.init( + id: id, + name: name, + permission: permission, + created: created, + expiration: expiration + ) + } +} + +// MARK: - IdentifiableManagedObject + +extension NewKeyManagedObject: IdentifiableManagedObject { } + +// MARK: - Store + +internal extension NSManagedObjectContext { + + @discardableResult + func insert(_ newKey: NewKey, for lock: LockManagedObject) throws -> NewKeyManagedObject { + + if let managedObject = try find(id: newKey.id, type: NewKeyManagedObject.self) { + assert(managedObject.lock == lock, "Key stored with conflicting lock") + return managedObject + } else { + return NewKeyManagedObject(newKey, lock: lock, context: self) + } + } + + @discardableResult + func insert(_ key: NewKey, for lock: UUID) throws -> NewKeyManagedObject { + + let managedObject = try find(id: lock, type: LockManagedObject.self) + ?? LockManagedObject(id: lock, name: "Lock", context: self) + return try insert(key, for: managedObject) + } +} diff --git a/Xcode/LockKit/Model/CoreData/PersistentContainer.swift b/Xcode/LockKit/Model/CoreData/PersistentContainer.swift new file mode 100644 index 00000000..f7f674dc --- /dev/null +++ b/Xcode/LockKit/Model/CoreData/PersistentContainer.swift @@ -0,0 +1,64 @@ +// +// PersistentStore.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/8/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreData + +public extension NSPersistentContainer { + + static var lock: NSPersistentContainer { + guard let appGroupURL = FileManager.default.containerURL(for: .lock) + else { fatalError("Couldn't get app group for \(AppGroup.lock.rawValue)") } + let container = NSPersistentContainer(name: "LockCache", managedObjectModel: .lock) + let storeDescription = NSPersistentStoreDescription(url: appGroupURL.appendingPathComponent("data.sqlite")) + storeDescription.shouldInferMappingModelAutomatically = true + storeDescription.shouldMigrateStoreAutomatically = true + container.persistentStoreDescriptions = [storeDescription] + return container + } +} + +internal extension NSPersistentContainer { + + func commit(_ block: @escaping (NSManagedObjectContext) throws -> ()) { + + performBackgroundTask { + do { + try block($0) + if $0.hasChanges { + try $0.save() + } + } catch { + log("⚠️ Unable to commit changes: \(error.localizedDescription)") + #if DEBUG + print(error) + #endif + assertionFailure("Core Data error") + return + } + } + } + + func commit(_ block: @escaping (NSManagedObjectContext) throws -> ()) async { + await performBackgroundTask { + do { + try block($0) + if $0.hasChanges { + try $0.save() + } + } catch { + log("⚠️ Unable to commit changes: \(error.localizedDescription)") + #if DEBUG + print(error) + #endif + assertionFailure("Core Data error") + return + } + } + } +} diff --git a/Xcode/LockKit/Model/CoreData/RemoveKeyEventManagedObject.swift b/Xcode/LockKit/Model/CoreData/RemoveKeyEventManagedObject.swift new file mode 100644 index 00000000..31034040 --- /dev/null +++ b/Xcode/LockKit/Model/CoreData/RemoveKeyEventManagedObject.swift @@ -0,0 +1,103 @@ +// +// RemoveKeyEventManagedObject.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/6/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreData +import CoreLock + +public final class RemoveKeyEventManagedObject: EventManagedObject { + + @nonobjc override class var eventType: LockEvent.EventType { return .removeKey } + + internal convenience init(_ value: LockEvent.RemoveKey, lock: LockManagedObject, context: NSManagedObjectContext) { + + self.init(context: context) + self.identifier = value.id + self.lock = lock + self.date = value.date + self.key = value.key + self.removedKey = value.removedKey + self.type = numericCast(value.type.rawValue) + } +} + +internal extension LockEvent.RemoveKey { + + init?(managedObject: RemoveKeyEventManagedObject) { + + guard let id = managedObject.identifier, + let date = managedObject.date, + let key = managedObject.key, + let removedKey = managedObject.removedKey, + let type = KeyType(rawValue: numericCast(managedObject.type)) + else { return nil } + + self.init(id: id, date: date, key: key, removedKey: removedKey, type: type) + } +} + +// MARK: - IdentifiableManagedObject + +extension RemoveKeyEventManagedObject: IdentifiableManagedObject { } + +// MARK: - Fetch + +public extension RemoveKeyEventManagedObject { + + /// Fetch the removed key specified by the event. + func removedKey(in context: NSManagedObjectContext) throws -> RemovedKey? { + + guard let removedKey = self.removedKey else { + assertionFailure("Missing key value") + return nil + } + + guard let type = KeyType(rawValue: numericCast(self.type)) else { + assertionFailure("Invalid key type") + return nil + } + + switch type { + case .key: + guard let managedObject = try context.find(id: removedKey, type: KeyManagedObject.self) + else { return nil } + return .key(managedObject) + case .newKey: + guard let managedObject = try context.find(id: removedKey, type: NewKeyManagedObject.self) + else { return nil } + return .newKey(managedObject) + } + } +} + +// MARK: - Supporting Types + +public extension RemoveKeyEventManagedObject { + + enum RemovedKey { + case key(KeyManagedObject) + case newKey(NewKeyManagedObject) + } +} + +public extension RemoveKeyEventManagedObject.RemovedKey { + + var id: UUID? { + switch self { + case let .key(managedObject): return managedObject.identifier + case let .newKey(managedObject): return managedObject.identifier + } + } + + var name: String? { + switch self { + case let .key(managedObject): return managedObject.name + case let .newKey(managedObject): return managedObject.name + } + } +} diff --git a/Xcode/LockKit/Model/CoreData/ScheduleManagedObject.swift b/Xcode/LockKit/Model/CoreData/ScheduleManagedObject.swift new file mode 100644 index 00000000..2edf6188 --- /dev/null +++ b/Xcode/LockKit/Model/CoreData/ScheduleManagedObject.swift @@ -0,0 +1,50 @@ +// +// ScheduleManagedObject.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/6/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreData +import CoreLock + +public final class ScheduleManagedObject: NSManagedObject { + + internal convenience init(_ value: Permission.Schedule, context: NSManagedObjectContext) { + + self.init(context: context) + self.expiry = value.expiry + self.intervalMin = numericCast(value.interval.rawValue.lowerBound) + self.intervalMax = numericCast(value.interval.rawValue.upperBound) + self.sunday = value.weekdays.sunday + self.monday = value.weekdays.monday + self.tuesday = value.weekdays.tuesday + self.wednesday = value.weekdays.wednesday + self.thursday = value.weekdays.thursday + self.friday = value.weekdays.friday + self.saturday = value.weekdays.saturday + } +} + +public extension Permission.Schedule { + + init?(managedObject: ScheduleManagedObject) { + guard let interval = Interval(rawValue: numericCast(managedObject.intervalMin) ... numericCast(managedObject.intervalMax)) + else { return nil } + self.init( + expiry: managedObject.expiry, + interval: interval, + weekdays: Weekdays( + sunday: managedObject.sunday, + monday: managedObject.monday, + tuesday: managedObject.tuesday, + wednesday: managedObject.wednesday, + thursday: managedObject.thursday, + friday: managedObject.friday, + saturday: managedObject.saturday + ) + ) + } +} diff --git a/Xcode/LockKit/Model/CoreData/SetupEventManagedObject.swift b/Xcode/LockKit/Model/CoreData/SetupEventManagedObject.swift new file mode 100644 index 00000000..a4ae68f6 --- /dev/null +++ b/Xcode/LockKit/Model/CoreData/SetupEventManagedObject.swift @@ -0,0 +1,43 @@ +// +// SetupEventManagedObject.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/6/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreData +import CoreLock + +public final class SetupEventManagedObject: EventManagedObject { + + @nonobjc override class var eventType: LockEvent.EventType { return .setup } + + internal convenience init(_ value: LockEvent.Setup, lock: LockManagedObject, context: NSManagedObjectContext) { + + self.init(context: context) + self.identifier = value.id + self.lock = lock + self.date = value.date + self.key = value.key + } +} + +public extension LockEvent.Setup { + + init?(managedObject: SetupEventManagedObject) { + + guard let id = managedObject.identifier, + let date = managedObject.date, + let key = managedObject.key + else { return nil } + + self.init(id: id, date: date, key: key) + } +} + +// MARK: - IdentifiableManagedObject + +extension SetupEventManagedObject: IdentifiableManagedObject { } + diff --git a/Xcode/LockKit/Model/CoreData/UnlockEventManagedObject.swift b/Xcode/LockKit/Model/CoreData/UnlockEventManagedObject.swift new file mode 100644 index 00000000..c6224dfa --- /dev/null +++ b/Xcode/LockKit/Model/CoreData/UnlockEventManagedObject.swift @@ -0,0 +1,44 @@ +// +// UnlockEventManagedObject.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/6/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreData +import CoreLock + +public final class UnlockEventManagedObject: EventManagedObject { + + @nonobjc override class var eventType: LockEvent.EventType { return .unlock } + + internal convenience init(_ value: LockEvent.Unlock, lock: LockManagedObject, context: NSManagedObjectContext) { + + self.init(context: context) + self.identifier = value.id + self.lock = lock + self.date = value.date + self.key = value.key + self.action = numericCast(value.action.rawValue) + } +} + +public extension LockEvent.Unlock { + + init?(managedObject: UnlockEventManagedObject) { + + guard let id = managedObject.identifier, + let date = managedObject.date, + let key = managedObject.key, + let action = UnlockAction(rawValue: numericCast(managedObject.action)) + else { return nil } + + self.init(id: id, date: date, key: key, action: action) + } +} + +// MARK: - IdentifiableManagedObject + +extension UnlockEventManagedObject: IdentifiableManagedObject { } diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 5f4233a3..2af754df 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -6,6 +6,7 @@ // import Foundation +import CoreData import Combine @_exported import Bluetooth @_exported import GATT @@ -41,26 +42,31 @@ public final class Store: ObservableObject { internal lazy var fileManager: FileManager.Lock = .shared + internal lazy var persistentContainer: NSPersistentContainer = .lock + + public lazy var managedObjectContext: NSManagedObjectContext = { + let context = self.persistentContainer.viewContext + context.automaticallyMergesChangesFromParent = true + context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump + context.undoManager = nil + return context + }() + + internal lazy var backgroundContext: NSManagedObjectContext = { + let context = self.persistentContainer.newBackgroundContext() + context.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump + context.undoManager = nil + return context + }() + // MARK: - Initialization private init() { - // setup logging central.log = { log("📲 Central: " + $0) } - - // observe state - Task { [weak self] in - while let self = self { - let newState = await self.central.state - let oldValue = self.state - if newState != oldValue { - self.state = newState - if newState == .poweredOn, isScanning == false { - await self.scan() - } - } - try await Task.sleep(nanoseconds: 1_000_000_000) - } - } + clearKeychainNewInstall() + loadPersistentStore() + lockCacheChanged() + observeBluetoothState() } } @@ -68,6 +74,25 @@ public final class Store: ObservableObject { public extension Store { + /// Clear keychain on newly installed app. + private func clearKeychainNewInstall() { + /* + if preferences.isAppInstalled == false { + preferences.isAppInstalled = true + do { try keychain.removeAll() } + catch { + log("⚠️ Unable to clear keychain: \(error.localizedDescription)") + DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { + #if DEBUG + print(error) + #endif + assertionFailure("Unable to clear keychain") + } + } + } + */ + } + /// Remove the specified lock from the cache and keychain. @discardableResult func remove(_ lock: UUID) -> Bool { @@ -141,10 +166,14 @@ public extension Store { } } -// MARK: - File Methods +// MARK: - Application Data public extension Store { + private func lockCacheChanged() { + updateCoreData() + } + var applicationData: ApplicationData { get { if let applicationData = fileManager.applicationData { @@ -157,7 +186,11 @@ public extension Store { } set { objectWillChange.send() + let oldValue = fileManager.applicationData fileManager.applicationData = newValue + if oldValue?.locks != newValue.locks { + lockCacheChanged() + } } } @@ -168,10 +201,65 @@ public extension Store { } } +// MARK: - CoreData + +private extension Store { + + func updateCoreData() { + let locks = self.applicationData.locks + backgroundContext.commit { + try $0.insert(locks) + } + } + + func loadPersistentStore() { + // load CoreData + let semaphore = DispatchSemaphore(value: 0) + persistentContainer.loadPersistentStores { (store, error) in + semaphore.signal() + if let error = error { + log("⚠️ Unable to load persistent store: \(error.localizedDescription)") + #if DEBUG + print(error) + #endif + if let url = store.url { + do { try FileManager.default.removeItem(at: url) } + catch { print(error) } + } + assertionFailure("Unable to load persistent store") + return + } + #if DEBUG + print("🗄 Loaded persistent store") + print(store) + #endif + } + let didTimeout = semaphore.wait(timeout: .now() + 5.0) == .timedOut + assert(didTimeout == false) + } +} + // MARK: - Bluetooth Methods public extension Store { + private func observeBluetoothState() { + // observe state + Task { [weak self] in + while let self = self { + let newState = await self.central.state + let oldValue = self.state + if newState != oldValue { + self.state = newState + if newState == .poweredOn, isScanning == false { + await self.scan() + } + } + try await Task.sleep(nanoseconds: 1_000_000_000) + } + } + } + /// The Bluetooth LE peripheral for the speciifed lock. subscript (peripheral id: UUID) -> NativeCentral.Peripheral? { return lockInformation.first(where: { $0.value.id == id })?.key diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 64dbeeee..5cb7d255 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -26,6 +26,21 @@ 6E21831F28D84A7300A622B3 /* SidebarLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21831E28D84A7300A622B3 /* SidebarLabel.swift */; }; 6E21832128D8501500A622B3 /* ProgressIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21832028D8501500A622B3 /* ProgressIndicatorView.swift */; }; 6E21832328D8D26300A622B3 /* LockDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21832228D8D26300A622B3 /* LockDetailView.swift */; }; + 6E21833528D8F3B500A622B3 /* SetupEventManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21832528D8F3B500A622B3 /* SetupEventManagedObject.swift */; }; + 6E21833628D8F3B500A622B3 /* ContactManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21832628D8F3B500A622B3 /* ContactManagedObject.swift */; }; + 6E21833728D8F3B500A622B3 /* PersistentContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21832728D8F3B500A622B3 /* PersistentContainer.swift */; }; + 6E21833828D8F3B500A622B3 /* NewKeyManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21832828D8F3B500A622B3 /* NewKeyManagedObject.swift */; }; + 6E21833928D8F3B500A622B3 /* ScheduleManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21832928D8F3B500A622B3 /* ScheduleManagedObject.swift */; }; + 6E21833A28D8F3B500A622B3 /* LockManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21832A28D8F3B500A622B3 /* LockManagedObject.swift */; }; + 6E21833B28D8F3B500A622B3 /* ConfirmNewKeyEventManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21832B28D8F3B500A622B3 /* ConfirmNewKeyEventManagedObject.swift */; }; + 6E21833C28D8F3B500A622B3 /* KeyManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21832C28D8F3B500A622B3 /* KeyManagedObject.swift */; }; + 6E21833D28D8F3B500A622B3 /* EventManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21832D28D8F3B500A622B3 /* EventManagedObject.swift */; }; + 6E21833E28D8F3B500A622B3 /* CreateNewKeyEventManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21832E28D8F3B500A622B3 /* CreateNewKeyEventManagedObject.swift */; }; + 6E21833F28D8F3B500A622B3 /* LockInformationManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21832F28D8F3B500A622B3 /* LockInformationManagedObject.swift */; }; + 6E21834028D8F3B500A622B3 /* RemoveKeyEventManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21833028D8F3B500A622B3 /* RemoveKeyEventManagedObject.swift */; }; + 6E21834128D8F3B500A622B3 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 6E21833128D8F3B500A622B3 /* Model.xcdatamodeld */; }; + 6E21834228D8F3B500A622B3 /* UnlockEventManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21833328D8F3B500A622B3 /* UnlockEventManagedObject.swift */; }; + 6E21834328D8F3B500A622B3 /* ManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21833428D8F3B500A622B3 /* ManagedObject.swift */; }; 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276BB28D708A000AF171B /* CoreLock */; }; 6E3276C328D70A3700AF171B /* HomeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E3276C228D70A3700AF171B /* HomeKit.framework */; }; 6E3276C628D70A3700AF171B /* RequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276C528D70A3700AF171B /* RequestHandler.swift */; }; @@ -94,6 +109,21 @@ 6E21831E28D84A7300A622B3 /* SidebarLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarLabel.swift; sourceTree = ""; }; 6E21832028D8501500A622B3 /* ProgressIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressIndicatorView.swift; sourceTree = ""; }; 6E21832228D8D26300A622B3 /* LockDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockDetailView.swift; sourceTree = ""; }; + 6E21832528D8F3B500A622B3 /* SetupEventManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetupEventManagedObject.swift; sourceTree = ""; }; + 6E21832628D8F3B500A622B3 /* ContactManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactManagedObject.swift; sourceTree = ""; }; + 6E21832728D8F3B500A622B3 /* PersistentContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistentContainer.swift; sourceTree = ""; }; + 6E21832828D8F3B500A622B3 /* NewKeyManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewKeyManagedObject.swift; sourceTree = ""; }; + 6E21832928D8F3B500A622B3 /* ScheduleManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduleManagedObject.swift; sourceTree = ""; }; + 6E21832A28D8F3B500A622B3 /* LockManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockManagedObject.swift; sourceTree = ""; }; + 6E21832B28D8F3B500A622B3 /* ConfirmNewKeyEventManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfirmNewKeyEventManagedObject.swift; sourceTree = ""; }; + 6E21832C28D8F3B500A622B3 /* KeyManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyManagedObject.swift; sourceTree = ""; }; + 6E21832D28D8F3B500A622B3 /* EventManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventManagedObject.swift; sourceTree = ""; }; + 6E21832E28D8F3B500A622B3 /* CreateNewKeyEventManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateNewKeyEventManagedObject.swift; sourceTree = ""; }; + 6E21832F28D8F3B500A622B3 /* LockInformationManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockInformationManagedObject.swift; sourceTree = ""; }; + 6E21833028D8F3B500A622B3 /* RemoveKeyEventManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoveKeyEventManagedObject.swift; sourceTree = ""; }; + 6E21833228D8F3B500A622B3 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; + 6E21833328D8F3B500A622B3 /* UnlockEventManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnlockEventManagedObject.swift; sourceTree = ""; }; + 6E21833428D8F3B500A622B3 /* ManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedObject.swift; sourceTree = ""; }; 6E3276BA28D7088D00AF171B /* SmartLock */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SmartLock; path = ..; sourceTree = ""; }; 6E3276C128D70A3700AF171B /* MatterLock.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MatterLock.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6E3276C228D70A3700AF171B /* HomeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HomeKit.framework; path = Library/Frameworks/HomeKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -150,6 +180,28 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 6E21832428D8F3B500A622B3 /* CoreData */ = { + isa = PBXGroup; + children = ( + 6E21833128D8F3B500A622B3 /* Model.xcdatamodeld */, + 6E21832B28D8F3B500A622B3 /* ConfirmNewKeyEventManagedObject.swift */, + 6E21832628D8F3B500A622B3 /* ContactManagedObject.swift */, + 6E21832E28D8F3B500A622B3 /* CreateNewKeyEventManagedObject.swift */, + 6E21832D28D8F3B500A622B3 /* EventManagedObject.swift */, + 6E21832C28D8F3B500A622B3 /* KeyManagedObject.swift */, + 6E21832F28D8F3B500A622B3 /* LockInformationManagedObject.swift */, + 6E21832A28D8F3B500A622B3 /* LockManagedObject.swift */, + 6E21833428D8F3B500A622B3 /* ManagedObject.swift */, + 6E21832828D8F3B500A622B3 /* NewKeyManagedObject.swift */, + 6E21832728D8F3B500A622B3 /* PersistentContainer.swift */, + 6E21833028D8F3B500A622B3 /* RemoveKeyEventManagedObject.swift */, + 6E21832928D8F3B500A622B3 /* ScheduleManagedObject.swift */, + 6E21832528D8F3B500A622B3 /* SetupEventManagedObject.swift */, + 6E21833328D8F3B500A622B3 /* UnlockEventManagedObject.swift */, + ); + path = CoreData; + sourceTree = ""; + }; 6E3276B928D7088D00AF171B /* Packages */ = { isa = PBXGroup; children = ( @@ -191,15 +243,16 @@ 6E3276DA28D7136400AF171B /* Model */ = { isa = PBXGroup; children = ( + 6E21832428D8F3B500A622B3 /* CoreData */, 6E21830D28D7FF2400A622B3 /* AppGroup.swift */, + 6E21831428D80FF900A622B3 /* ApplicationData.swift */, 6E3276DB28D7195400AF171B /* Central.swift */, 6E21830028D7C37500A622B3 /* Error.swift */, + 6E21830B28D7FEF600A622B3 /* FileManager.swift */, + 6E21831228D80FDD00A622B3 /* JSON.swift */, 6E21830F28D80DCD00A622B3 /* Keychain.swift */, - 6E21831428D80FF900A622B3 /* ApplicationData.swift */, 6E21831628D8116C00A622B3 /* NewKey.swift */, 6E21831728D8116C00A622B3 /* NewKeyDocument.swift */, - 6E21830B28D7FEF600A622B3 /* FileManager.swift */, - 6E21831228D80FDD00A622B3 /* JSON.swift */, 6E21830628D7D08300A622B3 /* Permission.swift */, 6E3276D128D70CE100AF171B /* Store.swift */, ); @@ -481,21 +534,36 @@ 6E21831928D8116C00A622B3 /* NewKeyDocument.swift in Sources */, 6ED248CC28D7A3BF00F78D07 /* ActivityIndicatorView.swift in Sources */, 6E21830528D7C51900A622B3 /* Log.swift in Sources */, + 6E21833B28D8F3B500A622B3 /* ConfirmNewKeyEventManagedObject.swift in Sources */, 6E21830C28D7FEF600A622B3 /* FileManager.swift in Sources */, 6E4CB61028D7867700116573 /* LockRowView.swift in Sources */, 6E21831828D8116C00A622B3 /* NewKey.swift in Sources */, 6E21831028D80DCD00A622B3 /* Keychain.swift in Sources */, + 6E21833C28D8F3B500A622B3 /* KeyManagedObject.swift in Sources */, 6E21830128D7C37500A622B3 /* Error.swift in Sources */, + 6E21833E28D8F3B500A622B3 /* CreateNewKeyEventManagedObject.swift in Sources */, 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */, 6E21830728D7D08300A622B3 /* Permission.swift in Sources */, + 6E21833F28D8F3B500A622B3 /* LockInformationManagedObject.swift in Sources */, + 6E21833D28D8F3B500A622B3 /* EventManagedObject.swift in Sources */, + 6E21834128D8F3B500A622B3 /* Model.xcdatamodeld in Sources */, + 6E21833728D8F3B500A622B3 /* PersistentContainer.swift in Sources */, + 6E21834228D8F3B500A622B3 /* UnlockEventManagedObject.swift in Sources */, + 6E21833528D8F3B500A622B3 /* SetupEventManagedObject.swift in Sources */, + 6E21834328D8F3B500A622B3 /* ManagedObject.swift in Sources */, + 6E21834028D8F3B500A622B3 /* RemoveKeyEventManagedObject.swift in Sources */, + 6E21833628D8F3B500A622B3 /* ContactManagedObject.swift in Sources */, 6E3276D228D70CE100AF171B /* Store.swift in Sources */, 6E4CB61A28D788AA00116573 /* UIStyleKit.swift in Sources */, 6E21830E28D7FF2400A622B3 /* AppGroup.swift in Sources */, 6E21831528D80FF900A622B3 /* ApplicationData.swift in Sources */, + 6E21833A28D8F3B500A622B3 /* LockManagedObject.swift in Sources */, 6E21830328D7C47500A622B3 /* Appearance.swift in Sources */, 6E3276DC28D7195400AF171B /* Central.swift in Sources */, + 6E21833928D8F3B500A622B3 /* ScheduleManagedObject.swift in Sources */, 6E21831328D80FDD00A622B3 /* JSON.swift in Sources */, 6E21832128D8501500A622B3 /* ProgressIndicatorView.swift in Sources */, + 6E21833828D8F3B500A622B3 /* NewKeyManagedObject.swift in Sources */, 6E4CB61B28D788AA00116573 /* PermissionIconView.swift in Sources */, 6E4CB62128D7907200116573 /* PermissionIconViewUIView.swift in Sources */, ); @@ -924,6 +992,19 @@ productName = GATT; }; /* End XCSwiftPackageProductDependency section */ + +/* Begin XCVersionGroup section */ + 6E21833128D8F3B500A622B3 /* Model.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + 6E21833228D8F3B500A622B3 /* Model.xcdatamodel */, + ); + currentVersion = 6E21833228D8F3B500A622B3 /* Model.xcdatamodel */; + path = Model.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ }; rootObject = 6EA7767928D7061600018FA3 /* Project object */; } diff --git a/Xcode/SmartLock/Model/SmartLock.xcdatamodeld/.xccurrentversion b/Xcode/SmartLock/Model/SmartLock.xcdatamodeld/.xccurrentversion deleted file mode 100644 index 0c67376e..00000000 --- a/Xcode/SmartLock/Model/SmartLock.xcdatamodeld/.xccurrentversion +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/Xcode/SmartLock/Model/SmartLock.xcdatamodeld/SmartLock.xcdatamodel/contents b/Xcode/SmartLock/Model/SmartLock.xcdatamodeld/SmartLock.xcdatamodel/contents deleted file mode 100644 index 9ed2921a..00000000 --- a/Xcode/SmartLock/Model/SmartLock.xcdatamodeld/SmartLock.xcdatamodel/contents +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file From 10b03c45a52aac0bffefec853cffec481cfccf64 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 13:38:01 -0700 Subject: [PATCH 077/229] [App] Fixed unlocking --- Xcode/LockKit/Extensions/Task.swift | 13 +++ Xcode/LockKit/Model/Store.swift | 104 ++++++++++++++++++---- Xcode/LockKit/View/LockDetailView.swift | 23 +++-- Xcode/SmartLock.xcodeproj/project.pbxproj | 12 +++ 4 files changed, 128 insertions(+), 24 deletions(-) create mode 100644 Xcode/LockKit/Extensions/Task.swift diff --git a/Xcode/LockKit/Extensions/Task.swift b/Xcode/LockKit/Extensions/Task.swift new file mode 100644 index 00000000..edbfe203 --- /dev/null +++ b/Xcode/LockKit/Extensions/Task.swift @@ -0,0 +1,13 @@ +// +// Task.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/19/22. +// + +internal extension Task where Success == Never, Failure == Never { + + static func sleep(timeInterval: Double) async throws { + try await sleep(nanoseconds: UInt64(timeInterval * Double(1_000_000_000))) + } +} diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 2af754df..0be5ec86 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -328,11 +328,61 @@ public extension Store { func scan(duration: TimeInterval) async { await scan() Task { - try? await Task.sleep(nanoseconds: UInt64(duration) * 1_000_000_000) + try? await Task.sleep(timeInterval: duration) stopScanning() } } + func device( + for id: UUID, + scanDuration duration: TimeInterval = 2.0 + ) async throws -> NativeCentral.Peripheral? { + if let peripheral = self[peripheral: id] { + return peripheral + } else { + let filterDuplicates = true //preferences.filterDuplicates + let stream = central.scan( + with: [LockService.uuid], + filterDuplicates: filterDuplicates + ) + Task { + try? await Task.sleep(timeInterval: duration) + stream.stop() + } + for try await scanData in stream { + guard let serviceUUIDs = scanData.advertisementData.serviceUUIDs, + serviceUUIDs.contains(LockService.uuid) + else { continue } + self.peripherals[scanData.peripheral] = scanData + let peripheral = scanData.peripheral + // if found and information has cached, stop scanning + if let information = lockInformation[peripheral], + information.id == id { + stream.stop() + return peripheral // return first found device + } + } + // scan stopped due to timeout + for peripheral in peripherals.keys { + // skip known locks that are not the targeted device + if let information = lockInformation[peripheral] { + guard information.id == id else { continue } + } + // request information + do { + let _ = try await self.readInformation(for: peripheral) + } catch { + log("⚠️ Could not read information: \(error.localizedDescription)") + continue // ignore + } + if let foundDevice = self[peripheral: id] { + return foundDevice + } + } + return self[peripheral: id] + } + } + func stopScanning() { scanStream?.stop() scanStream = nil @@ -385,17 +435,25 @@ public extension Store { } func unlock( - for lock: DarwinCentral.Peripheral, + for lock: UUID, action: UnlockAction = .default ) async throws { + stopScanning() // get lock key - let key = try self.key(for: lock) + guard let key = self.key(for: lock) else { + throw LockError.noKey(lock: lock) + } + // scan for lock + guard let peripheral = try await self.device(for: lock) else { + throw LockError.notInRange(lock: lock) + } + // connect to device try await central.unlock( action, using: key, - for: lock + for: peripheral ) - log("Unlocked") + log("Unlocked \(lock)") } func newKey( @@ -455,8 +513,13 @@ public extension Store { id: newKeyInvitation.key.id, secret: newKeyInvitation.secret ) - guard let (peripheral, information) = lockInformation.first(where: { $0.value.id == newKeyInvitation.lock }) else { - fatalError() // FIXME: + let lock = newKeyInvitation.lock + guard let peripheral = try await device(for: lock) else { + throw LockError.notInRange(lock: lock) + } + guard let information = lockInformation[peripheral] else { + assertionFailure("Should have information cached") + throw LockError.unknownLock(peripheral) } // BLE request try await central.confirmKey( @@ -497,26 +560,27 @@ public extension Store { secret: keyData ) - //let context = backgroundContext + let context = backgroundContext // BLE request + let centralLog = central.log try await central.connection(for: lock) { - let stream = try await $0.listKeys(using: key, log: { log("📲 Central: " + $0) }) + let stream = try await $0.listKeys(using: key, log: centralLog) var list = KeysList() for try await notification in stream { list.append(notification.key) // call completion block - updateBlock(list, notification.isLast)/* + updateBlock(list, notification.isLast) await context.commit { (context) in try context.insert(notification.key, for: information.id) - }*/ + } } } // upload keys to cloud - #if os(iOS) //updateCloud() - #endif + + log("Listed keys for lock \(information.id)") return true } @@ -539,21 +603,20 @@ public extension Store { secret: keyData ) - let lockIdentifier = information.id - //let context = backgroundContext + let context = backgroundContext // BLE request - let log = central.log + let centralLog = central.log try await central.connection(for: lock) { - let stream = try await $0.listEvents(fetchRequest: fetchRequest, using: key, log: log) + let stream = try await $0.listEvents(fetchRequest: fetchRequest, using: key, log: centralLog) var events = [LockEvent]() for try await notification in stream { if let event = notification.event { events.append(event) - log?("Recieved event \(event.id)")/* + centralLog?("Recieved event \(event.id)") await context.commit { (context) in try context.insert(event, for: information.id) - }*/ + } } // call completion block updateBlock(events, notification.isLast) @@ -578,6 +641,9 @@ public extension Store { } #endif */ + + log("Listed events for lock \(information.id)") + return true } } diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 37c887c3..e72a2c53 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -49,9 +49,12 @@ private extension LockDetailView { store.lockInformation.first(where: { $0.value.id == id })?.value } - func unlock() { - Task { - //store.unlock(for:action:) + func unlock() async { + do { + try await store.unlock(for: id, action: .default) + } + catch { + log("⚠️ Unable to unlock \(id)") } } @@ -79,7 +82,10 @@ extension LockDetailView { @State var showID = false - let unlock: () -> () + let unlock: () async -> () + + @State + private var enableActions = true private static let dateFormatter: DateFormatter = { let formatter = DateFormatter() @@ -98,10 +104,17 @@ extension LockDetailView { HStack { Spacer() // unlock button - Button(action: unlock, label: { + Button(action: { + //enableActions = false + Task { + await unlock() + //enableActions = true + } + }, label: { PermissionIconView(permission: cache.key.permission.type) .frame(width: 150, height: 150, alignment: .center) }) + //.disabled(enableActions == false) .padding(30) Spacer() } diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 5cb7d255..41d68cf4 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -41,6 +41,7 @@ 6E21834128D8F3B500A622B3 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 6E21833128D8F3B500A622B3 /* Model.xcdatamodeld */; }; 6E21834228D8F3B500A622B3 /* UnlockEventManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21833328D8F3B500A622B3 /* UnlockEventManagedObject.swift */; }; 6E21834328D8F3B500A622B3 /* ManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21833428D8F3B500A622B3 /* ManagedObject.swift */; }; + 6E21834628D8FC4300A622B3 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21834528D8FC4300A622B3 /* Task.swift */; }; 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276BB28D708A000AF171B /* CoreLock */; }; 6E3276C328D70A3700AF171B /* HomeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E3276C228D70A3700AF171B /* HomeKit.framework */; }; 6E3276C628D70A3700AF171B /* RequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276C528D70A3700AF171B /* RequestHandler.swift */; }; @@ -124,6 +125,7 @@ 6E21833228D8F3B500A622B3 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; 6E21833328D8F3B500A622B3 /* UnlockEventManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnlockEventManagedObject.swift; sourceTree = ""; }; 6E21833428D8F3B500A622B3 /* ManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedObject.swift; sourceTree = ""; }; + 6E21834528D8FC4300A622B3 /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = ""; }; 6E3276BA28D7088D00AF171B /* SmartLock */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SmartLock; path = ..; sourceTree = ""; }; 6E3276C128D70A3700AF171B /* MatterLock.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MatterLock.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6E3276C228D70A3700AF171B /* HomeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HomeKit.framework; path = Library/Frameworks/HomeKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -202,6 +204,14 @@ path = CoreData; sourceTree = ""; }; + 6E21834428D8FC3700A622B3 /* Extensions */ = { + isa = PBXGroup; + children = ( + 6E21834528D8FC4300A622B3 /* Task.swift */, + ); + path = Extensions; + sourceTree = ""; + }; 6E3276B928D7088D00AF171B /* Packages */ = { isa = PBXGroup; children = ( @@ -341,6 +351,7 @@ children = ( 6EA7769F28D707FE00018FA3 /* LockKit.h */, 6E21830428D7C51900A622B3 /* Log.swift */, + 6E21834428D8FC3700A622B3 /* Extensions */, 6E4CB60F28D7866600116573 /* View */, 6E3276DA28D7136400AF171B /* Model */, ); @@ -555,6 +566,7 @@ 6E21833628D8F3B500A622B3 /* ContactManagedObject.swift in Sources */, 6E3276D228D70CE100AF171B /* Store.swift in Sources */, 6E4CB61A28D788AA00116573 /* UIStyleKit.swift in Sources */, + 6E21834628D8FC4300A622B3 /* Task.swift in Sources */, 6E21830E28D7FF2400A622B3 /* AppGroup.swift in Sources */, 6E21831528D80FF900A622B3 /* ApplicationData.swift in Sources */, 6E21833A28D8F3B500A622B3 /* LockManagedObject.swift in Sources */, From a9b7d30664419c3470edd5a712f04745ba488fa0 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 14:12:51 -0700 Subject: [PATCH 078/229] [App] Require FaceID for unlocking --- .../Extensions/LocalAuthentication.swift | 23 +++++++++++++++++++ Xcode/LockKit/Model/Store.swift | 8 +++---- Xcode/LockKit/View/LockDetailView.swift | 7 ++++++ Xcode/SmartLock.xcodeproj/project.pbxproj | 12 ++++++++-- 4 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 Xcode/LockKit/Extensions/LocalAuthentication.swift diff --git a/Xcode/LockKit/Extensions/LocalAuthentication.swift b/Xcode/LockKit/Extensions/LocalAuthentication.swift new file mode 100644 index 00000000..052600ed --- /dev/null +++ b/Xcode/LockKit/Extensions/LocalAuthentication.swift @@ -0,0 +1,23 @@ +// +// LocalAuthentication.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/19/22. +// + +#if canImport(LocalAuthentication) +import Foundation +import LocalAuthentication + +extension LAContext { + + func canEvaluate(policy: LAPolicy) throws -> Bool { + var error: NSError? + let result = canEvaluatePolicy(policy, error: &error) + if let error = error { + throw error + } + return result + } +} +#endif diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 0be5ec86..68c73d23 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -255,7 +255,7 @@ public extension Store { await self.scan() } } - try await Task.sleep(nanoseconds: 1_000_000_000) + try await Task.sleep(timeInterval: 1) } } } @@ -290,7 +290,7 @@ public extension Store { serviceUUIDs.contains(LockService.uuid) else { continue } // cache found device - try? await Task.sleep(nanoseconds: 200_000_000) + try? await Task.sleep(timeInterval: 0.5) self.peripherals[scanData.peripheral] = scanData } } catch { @@ -305,9 +305,9 @@ public extension Store { .keys .filter { !self.lockInformation.keys.contains($0) } } - try? await Task.sleep(nanoseconds: 3 * 1_000_000_000) + try? await Task.sleep(timeInterval: 3) while self.isScanning, loading().isEmpty { - try? await Task.sleep(nanoseconds: 2 * 1_000_000_000) + try? await Task.sleep(timeInterval: 2) } // stop scanning and load info for unknown devices stopScanning() diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index e72a2c53..21a41b44 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -7,6 +7,7 @@ import SwiftUI import CoreLock +import LocalAuthentication public struct LockDetailView: View { @@ -50,7 +51,13 @@ private extension LockDetailView { } func unlock() async { + let authentication = LAContext() + let policy: LAPolicy = .deviceOwnerAuthenticationWithBiometrics + let reason = NSLocalizedString("Biometrics are needed to unlock", comment: "") do { + if try authentication.canEvaluate(policy: policy) { + try await authentication.evaluatePolicy(policy, localizedReason: reason) + } try await store.unlock(for: id, action: .default) } catch { diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 41d68cf4..a41305f6 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -42,6 +42,8 @@ 6E21834228D8F3B500A622B3 /* UnlockEventManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21833328D8F3B500A622B3 /* UnlockEventManagedObject.swift */; }; 6E21834328D8F3B500A622B3 /* ManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21833428D8F3B500A622B3 /* ManagedObject.swift */; }; 6E21834628D8FC4300A622B3 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21834528D8FC4300A622B3 /* Task.swift */; }; + 6E21834828D90CB800A622B3 /* EventsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21834728D90CB800A622B3 /* EventsView.swift */; }; + 6E21834A28D90F7A00A622B3 /* LocalAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21834928D90F7A00A622B3 /* LocalAuthentication.swift */; }; 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276BB28D708A000AF171B /* CoreLock */; }; 6E3276C328D70A3700AF171B /* HomeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E3276C228D70A3700AF171B /* HomeKit.framework */; }; 6E3276C628D70A3700AF171B /* RequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276C528D70A3700AF171B /* RequestHandler.swift */; }; @@ -126,6 +128,8 @@ 6E21833328D8F3B500A622B3 /* UnlockEventManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnlockEventManagedObject.swift; sourceTree = ""; }; 6E21833428D8F3B500A622B3 /* ManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedObject.swift; sourceTree = ""; }; 6E21834528D8FC4300A622B3 /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = ""; }; + 6E21834728D90CB800A622B3 /* EventsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsView.swift; sourceTree = ""; }; + 6E21834928D90F7A00A622B3 /* LocalAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthentication.swift; sourceTree = ""; }; 6E3276BA28D7088D00AF171B /* SmartLock */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SmartLock; path = ..; sourceTree = ""; }; 6E3276C128D70A3700AF171B /* MatterLock.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MatterLock.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6E3276C228D70A3700AF171B /* HomeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HomeKit.framework; path = Library/Frameworks/HomeKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -208,6 +212,7 @@ isa = PBXGroup; children = ( 6E21834528D8FC4300A622B3 /* Task.swift */, + 6E21834928D90F7A00A622B3 /* LocalAuthentication.swift */, ); path = Extensions; sourceTree = ""; @@ -277,6 +282,7 @@ 6E3276E528D782B900AF171B /* LockRowView.swift */, 6E4CB61928D788AA00116573 /* PermissionIconView.swift */, 6E21832228D8D26300A622B3 /* LockDetailView.swift */, + 6E21834728D90CB800A622B3 /* EventsView.swift */, ); path = View; sourceTree = ""; @@ -563,9 +569,11 @@ 6E21833528D8F3B500A622B3 /* SetupEventManagedObject.swift in Sources */, 6E21834328D8F3B500A622B3 /* ManagedObject.swift in Sources */, 6E21834028D8F3B500A622B3 /* RemoveKeyEventManagedObject.swift in Sources */, + 6E21834A28D90F7A00A622B3 /* LocalAuthentication.swift in Sources */, 6E21833628D8F3B500A622B3 /* ContactManagedObject.swift in Sources */, 6E3276D228D70CE100AF171B /* Store.swift in Sources */, 6E4CB61A28D788AA00116573 /* UIStyleKit.swift in Sources */, + 6E21834828D90CB800A622B3 /* EventsView.swift in Sources */, 6E21834628D8FC4300A622B3 /* Task.swift in Sources */, 6E21830E28D7FF2400A622B3 /* AppGroup.swift in Sources */, 6E21831528D80FF900A622B3 /* ApplicationData.swift in Sources */, @@ -779,9 +787,9 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SmartLock/Info.plist; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bluetooth is used to scan for nearby locks in the background."; INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Bluetooth is needed to connect to your smart lock."; + INFOPLIST_KEY_NSFaceIDUsageDescription = "Your FaceID is needed to unlock."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -822,9 +830,9 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SmartLock/Info.plist; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bluetooth is used to scan for nearby locks in the background."; INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Bluetooth is needed to connect to your smart lock."; + INFOPLIST_KEY_NSFaceIDUsageDescription = "Your FaceID is needed to unlock."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; From 960803e3ed7e3c2fa506f5cf4708838e5eee08e9 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 14:35:35 -0700 Subject: [PATCH 079/229] [App] Added `KeysView` --- Xcode/LockKit/Model/ApplicationData.swift | 17 +++ Xcode/SmartLock.xcodeproj/project.pbxproj | 4 + Xcode/SmartLock/View/KeysView.swift | 115 +++++++++++++++++++ Xcode/SmartLock/View/NearbyDevicesView.swift | 1 - Xcode/SmartLock/View/TabBarView.swift | 2 +- 5 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 Xcode/SmartLock/View/KeysView.swift diff --git a/Xcode/LockKit/Model/ApplicationData.swift b/Xcode/LockKit/Model/ApplicationData.swift index 1d1e39e4..19da0882 100644 --- a/Xcode/LockKit/Model/ApplicationData.swift +++ b/Xcode/LockKit/Model/ApplicationData.swift @@ -91,6 +91,14 @@ public struct LockCache: Codable, Equatable { /// Lock information. public var information: Information + + #if DEBUG + public init(key: Key, name: String, information: Information) { + self.key = key + self.name = name + self.information = information + } + #endif } public extension LockCache { @@ -109,6 +117,15 @@ public extension LockCache { /// Supported lock actions public var unlockActions: Set + + #if DEBUG + public init(buildVersion: LockBuildVersion, version: LockVersion, status: LockStatus, unlockActions: Set) { + self.buildVersion = buildVersion + self.version = version + self.status = status + self.unlockActions = unlockActions + } + #endif } } diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index a41305f6..972fd1b6 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -44,6 +44,7 @@ 6E21834628D8FC4300A622B3 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21834528D8FC4300A622B3 /* Task.swift */; }; 6E21834828D90CB800A622B3 /* EventsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21834728D90CB800A622B3 /* EventsView.swift */; }; 6E21834A28D90F7A00A622B3 /* LocalAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21834928D90F7A00A622B3 /* LocalAuthentication.swift */; }; + 6E21834C28D9140D00A622B3 /* KeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21834B28D9140D00A622B3 /* KeysView.swift */; }; 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276BB28D708A000AF171B /* CoreLock */; }; 6E3276C328D70A3700AF171B /* HomeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E3276C228D70A3700AF171B /* HomeKit.framework */; }; 6E3276C628D70A3700AF171B /* RequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276C528D70A3700AF171B /* RequestHandler.swift */; }; @@ -130,6 +131,7 @@ 6E21834528D8FC4300A622B3 /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = ""; }; 6E21834728D90CB800A622B3 /* EventsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsView.swift; sourceTree = ""; }; 6E21834928D90F7A00A622B3 /* LocalAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthentication.swift; sourceTree = ""; }; + 6E21834B28D9140D00A622B3 /* KeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeysView.swift; sourceTree = ""; }; 6E3276BA28D7088D00AF171B /* SmartLock */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SmartLock; path = ..; sourceTree = ""; }; 6E3276C128D70A3700AF171B /* MatterLock.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MatterLock.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6E3276C228D70A3700AF171B /* HomeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HomeKit.framework; path = Library/Frameworks/HomeKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -243,6 +245,7 @@ 6E21831E28D84A7300A622B3 /* SidebarLabel.swift */, 6E21831C28D834D200A622B3 /* ContentView.swift */, 6E2182F028D7B4FA00A622B3 /* SettingsView.swift */, + 6E21834B28D9140D00A622B3 /* KeysView.swift */, ); path = View; sourceTree = ""; @@ -536,6 +539,7 @@ 6E21831D28D834D200A622B3 /* ContentView.swift in Sources */, 6EA7768528D7061600018FA3 /* App.swift in Sources */, 6E2182EF28D7B37000A622B3 /* TabBarView.swift in Sources */, + 6E21834C28D9140D00A622B3 /* KeysView.swift in Sources */, 6E21831B28D8341000A622B3 /* SidebarView.swift in Sources */, 6E3276D028D70C9400AF171B /* NearbyDevicesView.swift in Sources */, 6E21831F28D84A7300A622B3 /* SidebarLabel.swift in Sources */, diff --git a/Xcode/SmartLock/View/KeysView.swift b/Xcode/SmartLock/View/KeysView.swift new file mode 100644 index 00000000..8b28cc38 --- /dev/null +++ b/Xcode/SmartLock/View/KeysView.swift @@ -0,0 +1,115 @@ +// +// KeysView.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/19/22. +// + +import SwiftUI +import LockKit + +struct KeysView: View { + + @EnvironmentObject + var store: Store + + var body: some View { + StateView(items: items) + } +} + +private extension KeysView { + + var items: [Item] { + store.applicationData.locks + .lazy + .sorted(by: { $0.value.key.created < $1.value.key.created }) + .map { Item(id: $0.key, cache: $0.value) } + } +} + +extension KeysView { + + struct StateView: View { + + let items: [Item] + + var body: some View { + list + .navigationTitle("Keys") + } + } +} + +private extension KeysView.StateView { + + var list: some View { + List(items) { (item) in + NavigationLink(destination: { + LockDetailView(id: item.id) + }, label: { + LockRowView(item) + }) + } + } +} + +extension KeysView { + + struct Item: Identifiable, Equatable { + + let id: UUID + + let cache: LockCache + } +} + +extension LockRowView { + + init(_ item: KeysView.Item) { + self.init( + image: .permission(item.cache.key.permission.type), + title: item.cache.name, + subtitle: item.cache.key.permission.type.localizedText + ) + } +} + +// MARK: - Preview + +#if DEBUG +struct KeysView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + KeysView.StateView(items: [ + .init( + id: UUID(), + cache: LockCache( + key: Key(permission: .owner), + name: "My Lock", + information: LockCache.Information( + buildVersion: .current, + version: .current, + status: .unlock, + unlockActions: [.default] + ) + ) + ), + .init( + id: UUID(), + cache: LockCache( + key: Key(permission: .admin), + name: "Key 2", + information: LockCache.Information( + buildVersion: .current, + version: .current, + status: .unlock, + unlockActions: [.default] + ) + ) + ) + ]) + } + } +} +#endif diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index f9b79f3b..c91b85ff 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -139,7 +139,6 @@ private extension NearbyDevicesView.StateView { } } } - .listStyle(.plain) } var trailingButtonItem: some View { diff --git a/Xcode/SmartLock/View/TabBarView.swift b/Xcode/SmartLock/View/TabBarView.swift index c1c2c312..b3cf2096 100644 --- a/Xcode/SmartLock/View/TabBarView.swift +++ b/Xcode/SmartLock/View/TabBarView.swift @@ -23,7 +23,7 @@ struct TabBarView: View { // Keys NavigationView { - EmptyView() + KeysView() Text("Select a lock") } .tabItem { From 67764dda809d0e52553849a652dd45f31bc143f7 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 14:37:38 -0700 Subject: [PATCH 080/229] [CoreLock] Conform `LockEvent` to `Identifiable` --- Sources/CoreLock/Event.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/CoreLock/Event.swift b/Sources/CoreLock/Event.swift index 44ef647f..e7036e05 100644 --- a/Sources/CoreLock/Event.swift +++ b/Sources/CoreLock/Event.swift @@ -8,7 +8,7 @@ import Foundation import TLVCoding -public enum LockEvent: Equatable { +public enum LockEvent: Equatable, Identifiable { case setup(Setup) case unlock(Unlock) @@ -166,7 +166,7 @@ extension LockEvent.EventType: TLVCodable { public extension LockEvent { - struct Setup: Codable, Equatable { + struct Setup: Codable, Equatable, Identifiable { public let id: UUID @@ -184,7 +184,7 @@ public extension LockEvent { } } - struct Unlock: Codable, Equatable { + struct Unlock: Codable, Equatable, Identifiable { public let id: UUID @@ -206,7 +206,7 @@ public extension LockEvent { } } - struct CreateNewKey: Codable, Equatable { + struct CreateNewKey: Codable, Equatable, Identifiable { public let id: UUID @@ -227,7 +227,7 @@ public extension LockEvent { } } - struct ConfirmNewKey: Codable, Equatable { + struct ConfirmNewKey: Codable, Equatable, Identifiable { public let id: UUID @@ -250,7 +250,7 @@ public extension LockEvent { } } - struct RemoveKey: Codable, Equatable { + struct RemoveKey: Codable, Equatable, Identifiable { public let id: UUID From 4f9b11c2ded0290c73c248f6a53e5d09efdb0449 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 15:25:22 -0700 Subject: [PATCH 081/229] [App] Added `EventsView` --- Xcode/LockKit/Model/Event.swift | 28 ++++ Xcode/LockKit/View/EventsView.swift | 153 ++++++++++++++++++++++ Xcode/LockKit/View/LockDetailView.swift | 8 +- Xcode/SmartLock.xcodeproj/project.pbxproj | 4 + Xcode/SmartLock/App.swift | 1 + Xcode/SmartLock/View/TabBarView.swift | 4 +- 6 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 Xcode/LockKit/Model/Event.swift create mode 100644 Xcode/LockKit/View/EventsView.swift diff --git a/Xcode/LockKit/Model/Event.swift b/Xcode/LockKit/Model/Event.swift new file mode 100644 index 00000000..24c8b6af --- /dev/null +++ b/Xcode/LockKit/Model/Event.swift @@ -0,0 +1,28 @@ +// +// Event.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/8/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreLock + +public extension LockEvent.EventType { + + var symbol: Character { + switch self { + case .setup: + return "🔐" + case .unlock: + return "🔓" + case .createNewKey: + return "🔏" + case .confirmNewKey: + return "🔑" + case .removeKey: + return "🗑" + } + } +} diff --git a/Xcode/LockKit/View/EventsView.swift b/Xcode/LockKit/View/EventsView.swift new file mode 100644 index 00000000..6ac99f68 --- /dev/null +++ b/Xcode/LockKit/View/EventsView.swift @@ -0,0 +1,153 @@ +// +// EventsView.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/19/22. +// + +import SwiftUI +import CoreLock + +public struct EventsView: View { + + @Environment(\.managedObjectContext) + var managedObjectContext + + let lock: UUID? + + @FetchRequest( + entity: EventManagedObject.entity(), + sortDescriptors: [ + NSSortDescriptor(keyPath: \EventManagedObject.date, ascending: false) + ], + predicate: nil + ) + var events: FetchedResults + + private static let dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .short + dateFormatter.timeStyle = .none + return dateFormatter + }() + + private static let timeFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .none + dateFormatter.timeStyle = .short + return dateFormatter + }() + + public var body: some View { + list + .navigationTitle("History") + .onAppear { + self._events.wrappedValue.nsPredicate = self.predicate + } + } + + public init(lock: UUID? = nil) { + self.lock = lock + } +} + +private extension EventsView { + + var predicate: NSPredicate? { + lock.flatMap { + NSPredicate( + format: "%K == %@", + #keyPath(EventManagedObject.lock.identifier), + $0 as NSUUID + ) + } + } + + var list: some View { + List(events) { + row(for: $0) + } + .refreshable { + reload() + } + .onAppear { + reload() + } + } + + func reload() { + + } + + func row(for managedObject: EventManagedObject) -> some View { + //guard let lock = managedObject.lock?.identifier else { + // fatalError("Missing identifier") + //} + let context = self.managedObjectContext + let eventType = type(of: managedObject).eventType + let action: String + var keyName: String + let key = try! managedObject.key(in: context) + if key == nil { + //needsKeys.insert(lock) + } + switch managedObject { + case is SetupEventManagedObject: + action = "Setup" //R.string.locksEventsViewController.eventsSetup() + keyName = key?.name ?? "" + case is UnlockEventManagedObject: + action = "Unlocked" //R.string.locksEventsViewController.eventsUnlocked() + keyName = key?.name ?? "" + case let event as CreateNewKeyEventManagedObject: + if let newKey = try! event.confirmKeyEvent(in: context)?.key(in: context)?.name { + action = "Shared \(newKey)" //R.string.locksEventsViewController.eventsSharedNamed(newKey) + } else if let newKey = try! event.newKey(in: context)?.name { + action = "Shared \(newKey)" //R.string.locksEventsViewController.eventsSharedNamed(newKey) + } else { + action = "Shared key" //R.string.locksEventsViewController.eventsShared() + //needsKeys.insert(lock) + } + keyName = key?.name ?? "" + case let event as ConfirmNewKeyEventManagedObject: + if let key = key, + let permission = PermissionType(rawValue: numericCast(key.permission)) { + action = "Recieved \(permission.localizedText) from \(key.name ?? "")" //R.string.locksEventsViewController.eventsCreated(key.name ?? "", permission.localizedText) + if let parentKey = try! event.createKeyEvent(in: context)?.key(in: context) { + keyName = "Shared by \(parentKey.name ?? "")" //R.string.locksEventsViewController.eventsSharedBy(parentKey.name ?? "") + } else { + keyName = "" + //needsKeys.insert(lock) + } + } else { + action = "Created key" //R.string.locksEventsViewController.eventsCreatedNamed() + keyName = "" + //needsKeys.insert(lock) + } + case let event as RemoveKeyEventManagedObject: + if let removedKey = try! event.removedKey(in: context)?.name { + action = "Removed key \(removedKey)" //R.string.locksEventsViewController.eventsRemovedNamed(removedKey) + } else { + action = "Removed key" //R.string.locksEventsViewController.eventsRemoved() + //needsKeys.insert(lock) + } + keyName = key?.name ?? "" + default: + fatalError("Invalid event \(managedObject)") + } + + //let lockName = managedObject.lock?.name ?? "" + //if self.lock == nil, lockName.isEmpty == false { + // keyName = keyName.isEmpty ? lockName : lockName + " - " + keyName + //} + + return LockRowView( + image: .emoji(eventType.symbol), + title: action, + subtitle: keyName, + trailing: ( + managedObject.date.flatMap { Self.dateFormatter.string(from: $0) } ?? "", + managedObject.date.flatMap { Self.timeFormatter.string(from: $0) } ?? "" + ) + ) + } +} diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 21a41b44..aa9ac2b8 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -14,6 +14,9 @@ public struct LockDetailView: View { @EnvironmentObject public var store: Store + @Environment(\.managedObjectContext) + var managedObjectContext + public let id: UUID public var body: some View { @@ -66,7 +69,8 @@ private extension LockDetailView { } var events: Int { - 2 + let fetchRequest = EventManagedObject.fetchRequest() + return (try? managedObjectContext.count(for: fetchRequest)) ?? 0 } var keys: Int { @@ -175,7 +179,7 @@ extension LockDetailView { .font(.body) .foregroundColor(.gray) NavigationLink(destination: { - Text("Events") + EventsView(lock: id) }, label: { HStack { Text("\(events) events") diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 972fd1b6..5b4446b2 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -45,6 +45,7 @@ 6E21834828D90CB800A622B3 /* EventsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21834728D90CB800A622B3 /* EventsView.swift */; }; 6E21834A28D90F7A00A622B3 /* LocalAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21834928D90F7A00A622B3 /* LocalAuthentication.swift */; }; 6E21834C28D9140D00A622B3 /* KeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21834B28D9140D00A622B3 /* KeysView.swift */; }; + 6E21834E28D91FDC00A622B3 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21834D28D91FDC00A622B3 /* Event.swift */; }; 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276BB28D708A000AF171B /* CoreLock */; }; 6E3276C328D70A3700AF171B /* HomeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E3276C228D70A3700AF171B /* HomeKit.framework */; }; 6E3276C628D70A3700AF171B /* RequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276C528D70A3700AF171B /* RequestHandler.swift */; }; @@ -132,6 +133,7 @@ 6E21834728D90CB800A622B3 /* EventsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsView.swift; sourceTree = ""; }; 6E21834928D90F7A00A622B3 /* LocalAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthentication.swift; sourceTree = ""; }; 6E21834B28D9140D00A622B3 /* KeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeysView.swift; sourceTree = ""; }; + 6E21834D28D91FDC00A622B3 /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; 6E3276BA28D7088D00AF171B /* SmartLock */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SmartLock; path = ..; sourceTree = ""; }; 6E3276C128D70A3700AF171B /* MatterLock.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MatterLock.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6E3276C228D70A3700AF171B /* HomeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HomeKit.framework; path = Library/Frameworks/HomeKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -272,6 +274,7 @@ 6E21831628D8116C00A622B3 /* NewKey.swift */, 6E21831728D8116C00A622B3 /* NewKeyDocument.swift */, 6E21830628D7D08300A622B3 /* Permission.swift */, + 6E21834D28D91FDC00A622B3 /* Event.swift */, 6E3276D128D70CE100AF171B /* Store.swift */, ); path = Model; @@ -579,6 +582,7 @@ 6E4CB61A28D788AA00116573 /* UIStyleKit.swift in Sources */, 6E21834828D90CB800A622B3 /* EventsView.swift in Sources */, 6E21834628D8FC4300A622B3 /* Task.swift in Sources */, + 6E21834E28D91FDC00A622B3 /* Event.swift in Sources */, 6E21830E28D7FF2400A622B3 /* AppGroup.swift in Sources */, 6E21831528D80FF900A622B3 /* ApplicationData.swift in Sources */, 6E21833A28D8F3B500A622B3 /* LockManagedObject.swift in Sources */, diff --git a/Xcode/SmartLock/App.swift b/Xcode/SmartLock/App.swift index f26709d8..3219b44d 100644 --- a/Xcode/SmartLock/App.swift +++ b/Xcode/SmartLock/App.swift @@ -15,6 +15,7 @@ struct LockApp: App { WindowGroup { ContentView() .environmentObject(Store.shared) + .environment(\.managedObjectContext, Store.shared.managedObjectContext) .onAppear { _ = LockApp.initialize } diff --git a/Xcode/SmartLock/View/TabBarView.swift b/Xcode/SmartLock/View/TabBarView.swift index b3cf2096..b6b00bfe 100644 --- a/Xcode/SmartLock/View/TabBarView.swift +++ b/Xcode/SmartLock/View/TabBarView.swift @@ -30,9 +30,9 @@ struct TabBarView: View { Label("Keys", systemImage: "key.fill") } - // Keys + // History NavigationView { - EmptyView() + EventsView() } .tabItem { Label("History", systemImage: "clock.fill") From 9765cce048d499f32ff71c0716b9cae7f2765f44 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 15:39:12 -0700 Subject: [PATCH 082/229] [App] Updated `EventsView` --- Xcode/LockKit/View/EventsView.swift | 35 ++++++++++++++++--------- Xcode/LockKit/View/LockDetailView.swift | 5 ++++ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/Xcode/LockKit/View/EventsView.swift b/Xcode/LockKit/View/EventsView.swift index 6ac99f68..742bdd04 100644 --- a/Xcode/LockKit/View/EventsView.swift +++ b/Xcode/LockKit/View/EventsView.swift @@ -10,6 +10,9 @@ import CoreLock public struct EventsView: View { + @EnvironmentObject + public var store: Store + @Environment(\.managedObjectContext) var managedObjectContext @@ -38,6 +41,9 @@ public struct EventsView: View { return dateFormatter }() + @State + private var needsKeys = Set() + public var body: some View { list .navigationTitle("History") @@ -63,6 +69,10 @@ private extension EventsView { } } + var locks: Set { + return self.lock.flatMap { [$0] } ?? Set(store.applicationData.locks.keys) + } + var list: some View { List(events) { row(for: $0) @@ -80,16 +90,17 @@ private extension EventsView { } func row(for managedObject: EventManagedObject) -> some View { - //guard let lock = managedObject.lock?.identifier else { - // fatalError("Missing identifier") - //} + var needsKeys = Set() + guard let lock = managedObject.lock?.identifier else { + fatalError("Missing identifier") + } let context = self.managedObjectContext let eventType = type(of: managedObject).eventType let action: String var keyName: String let key = try! managedObject.key(in: context) if key == nil { - //needsKeys.insert(lock) + needsKeys.insert(lock) } switch managedObject { case is SetupEventManagedObject: @@ -105,7 +116,7 @@ private extension EventsView { action = "Shared \(newKey)" //R.string.locksEventsViewController.eventsSharedNamed(newKey) } else { action = "Shared key" //R.string.locksEventsViewController.eventsShared() - //needsKeys.insert(lock) + needsKeys.insert(lock) } keyName = key?.name ?? "" case let event as ConfirmNewKeyEventManagedObject: @@ -116,29 +127,29 @@ private extension EventsView { keyName = "Shared by \(parentKey.name ?? "")" //R.string.locksEventsViewController.eventsSharedBy(parentKey.name ?? "") } else { keyName = "" - //needsKeys.insert(lock) + needsKeys.insert(lock) } } else { action = "Created key" //R.string.locksEventsViewController.eventsCreatedNamed() keyName = "" - //needsKeys.insert(lock) + needsKeys.insert(lock) } case let event as RemoveKeyEventManagedObject: if let removedKey = try! event.removedKey(in: context)?.name { action = "Removed key \(removedKey)" //R.string.locksEventsViewController.eventsRemovedNamed(removedKey) } else { action = "Removed key" //R.string.locksEventsViewController.eventsRemoved() - //needsKeys.insert(lock) + needsKeys.insert(lock) } keyName = key?.name ?? "" default: fatalError("Invalid event \(managedObject)") } - //let lockName = managedObject.lock?.name ?? "" - //if self.lock == nil, lockName.isEmpty == false { - // keyName = keyName.isEmpty ? lockName : lockName + " - " + keyName - //} + let lockName = managedObject.lock?.name ?? "" + if self.lock == nil, lockName.isEmpty == false { + keyName = keyName.isEmpty ? lockName : lockName + " - " + keyName + } return LockRowView( image: .emoji(eventType.symbol), diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index aa9ac2b8..c80d2d26 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -70,6 +70,11 @@ private extension LockDetailView { var events: Int { let fetchRequest = EventManagedObject.fetchRequest() + fetchRequest.predicate = NSPredicate( + format: "%K == %@", + #keyPath(EventManagedObject.lock.identifier), + id as NSUUID + ) return (try? managedObjectContext.count(for: fetchRequest)) ?? 0 } From d3bdfe2c079c1bb4d066608b212872d4c5748a1a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 19:20:48 -0700 Subject: [PATCH 083/229] [App] Added `Preferences` --- Xcode/LockKit/Model/Preferences.swift | 146 ++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 Xcode/LockKit/Model/Preferences.swift diff --git a/Xcode/LockKit/Model/Preferences.swift b/Xcode/LockKit/Model/Preferences.swift new file mode 100644 index 00000000..9c2e7ff8 --- /dev/null +++ b/Xcode/LockKit/Model/Preferences.swift @@ -0,0 +1,146 @@ +// +// Preferences.swift +// LockKit +// +// Created by Alsey Coleman Miller on 8/22/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import Combine + +/// Preferences +public final class Preferences { + + // MARK: - Preferences + + private let userDefaults: UserDefaults + + @available(iOS 13.0, watchOS 6.0, *) + public lazy var objectWillChange = ObservableObjectPublisher() + + // MARK: - Initialization + + public init(userDefaults: UserDefaults) { + self.userDefaults = userDefaults + } + + // MARK: - Methods + + private subscript (key: Key) -> T? { + get { userDefaults.object(forKey: key.rawValue) as? T } + set { + if #available(iOS 13.0, watchOSApplicationExtension 6.0, *) { + objectWillChange.send() + } + userDefaults.set(newValue, forKey: key.rawValue) + } + } +} + +// MARK: - ObservableObject + +@available(iOS 13.0, watchOS 6.0, *) +extension Preferences: ObservableObject { } + +// MARK: - App Group + +public extension Preferences { + + static let standard = Preferences(userDefaults: .standard) +} + +public extension Preferences { + + convenience init?(suiteName appGroup: AppGroup) { + guard let userDefaults = UserDefaults(suiteName: appGroup) + else { return nil } + self.init(userDefaults: userDefaults) + } +} + +public extension UserDefaults { + + /// Creates a user defaults object initialized with the defaults for the specified database name. + convenience init?(suiteName appGroup: AppGroup) { + self.init(suiteName: appGroup.rawValue) + } +} + +// MARK: - Accessors + +public extension Preferences { + + var isAppInstalled: Bool { + get { return self[.isAppInstalled] ?? false } + set { self[.isAppInstalled] = newValue } + } + + var isCloudBackupEnabled: Bool { + get { + let defaultValue = true + return self[.isCloudBackupEnabled] ?? defaultValue + } + set { self[.isCloudBackupEnabled] = newValue } + } + + var lastCloudUpdate: Date? { + get { return self[.lastCloudUpdate] } + set { self[.lastCloudUpdate] = newValue } + } + + var lastWatchUpdate: Date? { + get { return self[.lastWatchUpdate] } + set { self[.lastWatchUpdate] = newValue } + } + + var bluetoothTimeout: TimeInterval { + get { return self[.bluetoothTimeout] ?? 15.0 } + set { self[.bluetoothTimeout] = newValue } + } + + var scanDuration: TimeInterval { + get { return self[.scanDuration] ?? 3.0 } + set { self[.scanDuration] = newValue } + } + + var filterDuplicates: Bool { + get { return self[.filterDuplicates] ?? true } + set { self[.filterDuplicates] = newValue } + } + + var writeWithoutResponseTimeout: TimeInterval { + get { return self[.writeWithoutResponseTimeout] ?? 3.0 } + set { self[.writeWithoutResponseTimeout] = newValue } + } + + var showPowerAlert: Bool { + get { return self[.showPowerAlert] ?? false } + set { self[.showPowerAlert] = newValue } + } + + var monitorBluetoothNotifications: Bool { + get { return self[.monitorBluetoothNotifications] ?? false } + set { self[.monitorBluetoothNotifications] = newValue } + } +} + +// MARK: - Supporting Types + +public extension Preferences { + + enum Key: String, CaseIterable { + + case isAppInstalled + case isCloudBackupEnabled + case lastCloudUpdate + case lastWatchUpdate + + case bluetoothTimeout + case filterDuplicates + case showPowerAlert + case writeWithoutResponseTimeout + case scanDuration + case monitorBluetoothNotifications + } +} From 4fd54e9a8c396981526c2f6f20caff0bc27008a6 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 22:59:08 -0700 Subject: [PATCH 084/229] [App] Added CloudKit types --- .../Model/CoreData/LockManagedObject.swift | 5 +- .../Model/CoreData/ManagedObject.swift | 17 - Xcode/LockKit/Model/Store.swift | 21 +- .../Model/iCloud/CloudApplicationData.swift | 256 ++++++++ Xcode/LockKit/Model/iCloud/CloudEvent.swift | 180 ++++++ Xcode/LockKit/Model/iCloud/CloudKey.swift | 142 +++++ Xcode/LockKit/Model/iCloud/CloudKit.swift | 387 ++++++++++++ Xcode/LockKit/Model/iCloud/CloudLock.swift | 84 +++ Xcode/LockKit/Model/iCloud/CloudNewKey.swift | 143 +++++ .../Model/iCloud/CloudNewKeyInvitation.swift | 95 +++ .../Model/iCloud/CloudPermission.swift | 187 ++++++ Xcode/LockKit/Model/iCloud/CloudShare.swift | 233 ++++++++ Xcode/LockKit/Model/iCloud/CloudUser.swift | 72 +++ Xcode/LockKit/Model/iCloud/iCloud.swift | 556 ++++++++++++++++++ Xcode/SmartLock.xcodeproj/project.pbxproj | 73 +++ .../xcshareddata/swiftpm/Package.resolved | 9 + Xcode/SmartLock/Info.plist | 2 + Xcode/SmartLock/SmartLock.entitlements | 16 + 18 files changed, 2449 insertions(+), 29 deletions(-) create mode 100644 Xcode/LockKit/Model/iCloud/CloudApplicationData.swift create mode 100644 Xcode/LockKit/Model/iCloud/CloudEvent.swift create mode 100644 Xcode/LockKit/Model/iCloud/CloudKey.swift create mode 100644 Xcode/LockKit/Model/iCloud/CloudKit.swift create mode 100644 Xcode/LockKit/Model/iCloud/CloudLock.swift create mode 100644 Xcode/LockKit/Model/iCloud/CloudNewKey.swift create mode 100644 Xcode/LockKit/Model/iCloud/CloudNewKeyInvitation.swift create mode 100644 Xcode/LockKit/Model/iCloud/CloudPermission.swift create mode 100644 Xcode/LockKit/Model/iCloud/CloudShare.swift create mode 100644 Xcode/LockKit/Model/iCloud/CloudUser.swift create mode 100644 Xcode/LockKit/Model/iCloud/iCloud.swift diff --git a/Xcode/LockKit/Model/CoreData/LockManagedObject.swift b/Xcode/LockKit/Model/CoreData/LockManagedObject.swift index bc92f086..7aea9205 100644 --- a/Xcode/LockKit/Model/CoreData/LockManagedObject.swift +++ b/Xcode/LockKit/Model/CoreData/LockManagedObject.swift @@ -78,8 +78,7 @@ internal extension NSManagedObjectContext { } } } - /* - #if os(iOS) + @discardableResult func insert(_ cloudValue: CloudLock) throws -> LockManagedObject { @@ -95,6 +94,4 @@ internal extension NSManagedObjectContext { } return lockManagedObject } - #endif - */ } diff --git a/Xcode/LockKit/Model/CoreData/ManagedObject.swift b/Xcode/LockKit/Model/CoreData/ManagedObject.swift index c22dc95b..44517645 100644 --- a/Xcode/LockKit/Model/CoreData/ManagedObject.swift +++ b/Xcode/LockKit/Model/CoreData/ManagedObject.swift @@ -22,23 +22,6 @@ public extension NSManagedObjectModel { internal extension NSManagedObjectContext { - /// Wraps the block to allow for error throwing. - func performErrorBlockAndWait(_ block: @escaping () throws -> (T)) throws -> T { - - var blockError: Swift.Error? - var value: T! - performAndWait { - do { value = try block() } - catch { blockError = error } - return - } - - if let error = blockError { - throw error - } - return value - } - func commit(_ block: @escaping (NSManagedObjectContext) throws -> ()) { assert(concurrencyType == .privateQueueConcurrencyType) diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 68c73d23..ed4d575d 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -38,6 +38,8 @@ public final class Store: ObservableObject { private var scanStream: AsyncCentralScan? + public lazy var preferences = Preferences(suiteName: .lock)! + internal lazy var keychain = Keychain(service: .lock, accessGroup: .lock) internal lazy var fileManager: FileManager.Lock = .shared @@ -59,14 +61,18 @@ public final class Store: ObservableObject { return context }() + public lazy var cloud: CloudStore = .shared + // MARK: - Initialization private init() { central.log = { log("📲 Central: " + $0) } clearKeychainNewInstall() loadPersistentStore() - lockCacheChanged() observeBluetoothState() + Task { + await lockCacheChanged() + } } } @@ -76,7 +82,7 @@ public extension Store { /// Clear keychain on newly installed app. private func clearKeychainNewInstall() { - /* + if preferences.isAppInstalled == false { preferences.isAppInstalled = true do { try keychain.removeAll() } @@ -90,7 +96,6 @@ public extension Store { } } } - */ } /// Remove the specified lock from the cache and keychain. @@ -170,8 +175,8 @@ public extension Store { public extension Store { - private func lockCacheChanged() { - updateCoreData() + private func lockCacheChanged() async { + await updateCoreData() } var applicationData: ApplicationData { @@ -203,11 +208,11 @@ public extension Store { // MARK: - CoreData -private extension Store { +internal extension Store { - func updateCoreData() { + func updateCoreData() async { let locks = self.applicationData.locks - backgroundContext.commit { + await backgroundContext.commit { try $0.insert(locks) } } diff --git a/Xcode/LockKit/Model/iCloud/CloudApplicationData.swift b/Xcode/LockKit/Model/iCloud/CloudApplicationData.swift new file mode 100644 index 00000000..96514bfc --- /dev/null +++ b/Xcode/LockKit/Model/iCloud/CloudApplicationData.swift @@ -0,0 +1,256 @@ +// +// CloudApplicationData.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/11/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreLock +import CloudKit +import CloudKitCodable + +public extension ApplicationData { + + struct Cloud: Codable, Equatable { + public let id: Cloud.ID + public let created: Date + public var updated: Date + public var locks: [LockCache.Cloud] + } +} + +public extension ApplicationData.Cloud { + + init(_ value: ApplicationData, user: CloudUser.ID) { + + self.id = .init(rawValue: value.id) + self.created = value.created + self.updated = value.updated + self.locks = value.locks + .sorted(by: { $0.key.uuidString > $1.key.uuidString }) + .map { LockCache.Cloud(lock: $0.key, cache: $0.value, applicationData: value.id) } + } +} + +public extension ApplicationData { + + init?(_ cloud: Cloud) { + var locks = [UUID: LockCache]() + for lock in cloud.locks { + guard let value = LockCache(lock) + else { return nil } + locks[lock.id.rawValue] = value + } + self.init( + id: cloud.id.rawValue, + created: cloud.created, + updated: cloud.updated, + locks: locks + ) + } +} + +public extension ApplicationData.Cloud { + struct ID: RawRepresentable, Codable, Equatable, Hashable { + public let rawValue: UUID + public init(rawValue: UUID = UUID()) { + self.rawValue = rawValue + } + } +} + +extension ApplicationData.Cloud: CloudKitCodable { + public var cloudIdentifier: CloudKitIdentifier { + return id + } +} + +extension ApplicationData.Cloud.ID: CloudKitIdentifier { + + public static var cloudRecordType: CKRecord.RecordType { + return "ApplicationData" + } + + public init?(cloudRecordID: CKRecord.ID) { + let string = cloudRecordID.recordName + .replacingOccurrences(of: type(of: self).cloudRecordType + "/", with: "") + guard let rawValue = UUID(uuidString: string) + else { return nil } + self.init(rawValue: rawValue) + } + + public var cloudRecordID: CKRecord.ID { + return CKRecord.ID(recordName: type(of: self).cloudRecordType + "/" + rawValue.uuidString) + } +} + +public extension LockCache { + + struct Cloud: Codable, Equatable { + + /// Identifier + public let id: ID + + public let applicationData: ApplicationData.Cloud.ID + + /// Stored key for lock. + /// + /// Can only have one key per lock. + public let key: Key.Cloud + + /// User-friendly lock name + public var name: String + + /// Lock information. + public var information: LockCache.Information.Cloud + } +} + +internal extension LockCache.Cloud { + + init(lock: UUID, + cache: LockCache, + applicationData: UUID) { + + self.id = .init(rawValue: lock) + self.key = .init(cache.key, lock: lock) + self.applicationData = .init(rawValue: applicationData) + self.name = cache.name + self.information = .init(id: lock, value: cache.information) + } +} + +internal extension LockCache { + + init?(_ cloud: Cloud) { + guard let key = Key(cloud.key), + let information = Information(cloud.information) + else { return nil } + self.name = cloud.name + self.key = key + self.information = information + } +} + +public extension LockCache.Cloud { + struct ID: RawRepresentable, Codable, Equatable, Hashable { + public let rawValue: UUID + public init(rawValue: UUID = UUID()) { + self.rawValue = rawValue + } + } +} + +extension LockCache.Cloud: CloudKitCodable { + public var cloudIdentifier: CloudKitIdentifier { + return id + } + public var parentRecord: CloudKitIdentifier? { + return applicationData + } +} + +extension LockCache.Cloud.ID: CloudKitIdentifier { + + public static var cloudRecordType: CKRecord.RecordType { + return "LockCache" + } + + public init?(cloudRecordID: CKRecord.ID) { + let string = cloudRecordID.recordName + .replacingOccurrences(of: type(of: self).cloudRecordType + "/", with: "") + guard let rawValue = UUID(uuidString: string) + else { return nil } + self.init(rawValue: rawValue) + } + + public var cloudRecordID: CKRecord.ID { + return CKRecord.ID(recordName: type(of: self).cloudRecordType + "/" + rawValue.uuidString) + } +} + +public extension LockCache.Information { + + struct Cloud: Codable, Equatable { + + /// Identifier + public let id: ID + + /// Firmware build number + public var buildVersion: String + + /// Firmware version + public var version: String + + /// Device state + public var status: LockStatus + + /// Supported lock actions + public var unlockActions: Set + } +} + +internal extension LockCache.Information.Cloud { + + init(id: UUID, value: LockCache.Information) { + self.id = .init(rawValue: id) + self.buildVersion = value.buildVersion.description + self.version = value.version.description + self.status = value.status + self.unlockActions = value.unlockActions + } +} + +internal extension LockCache.Information { + + init?(_ cloud: LockCache.Information.Cloud) { + + guard let buildVersion = UInt64(cloud.buildVersion).flatMap(LockBuildVersion.init), + let version = LockVersion(rawValue: cloud.version) + else { return nil } + + self.buildVersion = buildVersion + self.version = version + self.status = cloud.status + self.unlockActions = cloud.unlockActions + } +} + +public extension LockCache.Information.Cloud { + struct ID: RawRepresentable, Codable, Equatable, Hashable { + public let rawValue: UUID + public init(rawValue: UUID = UUID()) { + self.rawValue = rawValue + } + } +} + +extension LockCache.Information.Cloud: CloudKitCodable { + public var cloudIdentifier: CloudKitIdentifier { + return id + } + public var parentRecord: CloudKitIdentifier? { + return LockCache.Cloud.ID(rawValue: id.rawValue) + } +} + +extension LockCache.Information.Cloud.ID: CloudKitIdentifier { + + public static var cloudRecordType: CKRecord.RecordType { + return "LockInformation" + } + + public init?(cloudRecordID: CKRecord.ID) { + let string = cloudRecordID.recordName + .replacingOccurrences(of: type(of: self).cloudRecordType + "/", with: "") + guard let rawValue = UUID(uuidString: string) + else { return nil } + self.init(rawValue: rawValue) + } + + public var cloudRecordID: CKRecord.ID { + return CKRecord.ID(recordName: type(of: self).cloudRecordType + "/" + rawValue.uuidString) + } +} diff --git a/Xcode/LockKit/Model/iCloud/CloudEvent.swift b/Xcode/LockKit/Model/iCloud/CloudEvent.swift new file mode 100644 index 00000000..4dc6c8b2 --- /dev/null +++ b/Xcode/LockKit/Model/iCloud/CloudEvent.swift @@ -0,0 +1,180 @@ +// +// LockEvent.Cloud.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/14/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CloudKit +import CloudKitCodable +import CoreLock + +public extension LockEvent { + + struct Cloud: Codable, Equatable { + + public let id: ID + + public let type: LockEvent.EventType + + public let lock: CloudLock.ID + + public let date: Date + + public let key: UUID + + public let newKey: UUID? + + public let removedKey: UUID? + + public let removedKeyType: KeyType? + + public let unlockAction: UnlockAction? + } +} + +internal extension LockEvent.Cloud { + + init(event: LockEvent, for lock: UUID) { + + self.lock = .init(rawValue: lock) + switch event { + case let .setup(event): + self.type = .setup + self.id = .init(rawValue: event.id) + self.date = event.date + self.key = event.key + self.newKey = nil + self.removedKey = nil + self.removedKeyType = nil + self.unlockAction = nil + case let .unlock(event): + self.type = .unlock + self.id = .init(rawValue: event.id) + self.date = event.date + self.key = event.key + self.unlockAction = event.action + self.newKey = nil + self.removedKey = nil + self.removedKeyType = nil + case let .createNewKey(event): + self.type = .createNewKey + self.id = .init(rawValue: event.id) + self.date = event.date + self.key = event.key + self.newKey = event.newKey + self.removedKey = nil + self.removedKeyType = nil + self.unlockAction = nil + case let .confirmNewKey(event): + self.type = .confirmNewKey + self.id = .init(rawValue: event.id) + self.date = event.date + self.key = event.key + self.newKey = event.newKey + self.removedKey = nil + self.removedKeyType = nil + self.unlockAction = nil + case let .removeKey(event): + self.type = .removeKey + self.id = .init(rawValue: event.id) + self.date = event.date + self.key = event.key + self.removedKey = event.removedKey + self.removedKeyType = .init(event.type) + self.newKey = nil + self.unlockAction = nil + } + } +} + +internal extension LockEvent { + + init?(_ cloud: LockEvent.Cloud) { + switch cloud.type { + case .setup: + self = .setup(.init(id: cloud.id.rawValue, date: cloud.date, key: cloud.key)) + case .unlock: + guard let action = cloud.unlockAction + else { return nil } + self = .unlock(.init(id: cloud.id.rawValue, date: cloud.date, key: cloud.key, action: action)) + case .createNewKey: + guard let newKey = cloud.newKey + else { return nil } + self = .createNewKey(.init(id: cloud.id.rawValue, date: cloud.date, key: cloud.key, newKey: newKey)) + case .confirmNewKey: + guard let newKey = cloud.newKey + else { return nil } + self = .confirmNewKey(.init(id: cloud.id.rawValue, date: cloud.date, newKey: newKey, key: cloud.key)) + case .removeKey: + guard let removedKey = cloud.removedKey, + let removedKeyType = cloud.removedKeyType + else { return nil } + self = .removeKey(.init(id: cloud.id.rawValue, date: cloud.date, key: cloud.key, removedKey: removedKey, type: removedKeyType)) + } + } +} + +public extension LockEvent.Cloud { + struct ID: RawRepresentable, Codable, Equatable, Hashable { + public let rawValue: UUID + public init(rawValue: UUID) { + self.rawValue = rawValue + } + } +} + +extension LockEvent.Cloud: CloudKitCodable { + public var cloudIdentifier: CloudKitIdentifier { + return id + } + public var parentRecord: CloudKitIdentifier? { + return lock + } +} + +extension LockEvent.Cloud.ID: CloudKitIdentifier { + + public static var cloudRecordType: CKRecord.RecordType { + return "Event" + } + + public init?(cloudRecordID: CKRecord.ID) { + let string = cloudRecordID.recordName + .replacingOccurrences(of: type(of: self).cloudRecordType + "/", with: "") + guard let rawValue = UUID(uuidString: string) + else { return nil } + self.init(rawValue: rawValue) + } + + public var cloudRecordID: CKRecord.ID { + return CKRecord.ID(recordName: type(of: self).cloudRecordType + "/" + rawValue.uuidString) + } +} + +// MARK: - CloudKit Fetch + +public extension CloudStore { + + func fetchEvents( + for lock: CloudLock.ID + ) -> AsyncThrowingMapSequence, LockEvent.Cloud> { + let database = container.privateCloudDatabase + let decoder = CloudKitDecoder(context: database) + let lockReference = CKRecord.Reference( + recordID: lock.cloudRecordID, + action: .none + ) + let query = CKQuery( + recordType: LockEvent.Cloud.ID.cloudRecordType, + predicate: NSPredicate(format: "%K == %@", "lock", lockReference) + ) + query.sortDescriptors = [ + .init(key: "date", ascending: false) // \LockEvent.Cloud.date + ] + return database.queryAll(query) + .map { try decoder.decode(LockEvent.Cloud.self, from: $0) } + } +} diff --git a/Xcode/LockKit/Model/iCloud/CloudKey.swift b/Xcode/LockKit/Model/iCloud/CloudKey.swift new file mode 100644 index 00000000..a5232f6d --- /dev/null +++ b/Xcode/LockKit/Model/iCloud/CloudKey.swift @@ -0,0 +1,142 @@ +// +// CloudKey.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/11/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreLock +import CloudKit +import CloudKitCodable + +public extension Key { + + struct Cloud: Codable, Equatable { + + /// The unique identifier of the key. + public let id: ID + + /// Lock this key belongs to. + public let lock: CloudLock.ID + + /// The name of the key. + public var name: String + + /// Date key was created. + public let created: Date + + /// Key's permissions. + public let permissionType: PermissionType + + /// Key Permission Schedule + public let schedule: Permission.Schedule.Cloud? + } +} + +public extension Key.Cloud { + + init(_ value: Key, lock: UUID) { + self.id = .init(rawValue: value.id) + self.lock = .init(rawValue: lock) + self.name = value.name + self.created = value.created + self.permissionType = value.permission.type + if case let .scheduled(schedule) = value.permission { + self.schedule = Permission.Schedule.Cloud(schedule, key: value.id, type: .key) + } else { + self.schedule = nil + } + } +} + +public extension Key { + + init?(_ cloud: Cloud) { + + let id = cloud.id.rawValue + let permission: Permission + + switch cloud.permissionType { + case .owner: + permission = .owner + case .admin: + permission = .admin + case .anytime: + permission = .anytime + case .scheduled: + guard let cloudSchedule = cloud.schedule, + let schedule = Permission.Schedule(cloudSchedule) + else { return nil } + permission = .scheduled(schedule) + } + + self.init( + id: id, + name: cloud.name, + created: cloud.created, + permission: permission + ) + } +} + +public extension Key.Cloud { + struct ID: RawRepresentable, Codable, Equatable, Hashable { + public let rawValue: UUID + public init(rawValue: UUID = UUID()) { + self.rawValue = rawValue + } + } +} + +extension Key.Cloud: CloudKitCodable { + public var cloudIdentifier: CloudKitIdentifier { + return id + } + public var parentRecord: CloudKitIdentifier? { + return lock + } +} + +extension Key.Cloud.ID: CloudKitIdentifier { + + public static var cloudRecordType: CKRecord.RecordType { + return "Key" + } + + public init?(cloudRecordID: CKRecord.ID) { + let string = cloudRecordID.recordName + .replacingOccurrences(of: type(of: self).cloudRecordType + "/", with: "") + guard let rawValue = UUID(uuidString: string) + else { return nil } + self.init(rawValue: rawValue) + } + + public var cloudRecordID: CKRecord.ID { + return CKRecord.ID(recordName: type(of: self).cloudRecordType + "/" + rawValue.uuidString) + } +} + +// MARK: - CloudKit Fetch + +public extension CloudStore { + + func fetchKeys(for lock: CloudLock.ID) -> AsyncThrowingMapSequence, Key.Cloud> { + let database = container.privateCloudDatabase + let decoder = CloudKitDecoder(context: database) + let lockReference = CKRecord.Reference( + recordID: lock.cloudRecordID, + action: .none + ) + let query = CKQuery( + recordType: Key.Cloud.ID.cloudRecordType, + predicate: NSPredicate(format: "%K == %@", "lock", lockReference) + ) + query.sortDescriptors = [ + .init(key: "created", ascending: false) // \Key.Cloud.created + ] + return database.queryAll(query) + .map { try decoder.decode(Key.Cloud.self, from: $0) } + } +} diff --git a/Xcode/LockKit/Model/iCloud/CloudKit.swift b/Xcode/LockKit/Model/iCloud/CloudKit.swift new file mode 100644 index 00000000..507d90c9 --- /dev/null +++ b/Xcode/LockKit/Model/iCloud/CloudKit.swift @@ -0,0 +1,387 @@ +// +// CloudKit.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/15/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CloudKit + +public extension CKContainer { + + convenience init(identifier: UbiquityContainerIdentifier) { + self.init(identifier: identifier.rawValue) + } +} + +public extension CKContainer { + + /// `iCloud.com.colemancda.Lock` CloudKit container. + static var lock: CKContainer { + struct Cache { + static let container = CKContainer(identifier: .lock) + } + return Cache.container + } +} + +internal extension CKContainer { + + func fetchUserRecordID() async throws -> CKRecord.ID { + return try await withCheckedThrowingContinuation { continuation in + self.fetchUserRecordID { (id, error) in + if let error = error { + continuation.resume(throwing: error) + } else if let id = id { + continuation.resume(returning: id) + } else { + assertionFailure() + continuation.resume(throwing: CKError(.internalError)) + } + } + } + } + + func discoverAllUserIdentities() -> AsyncThrowingStream { + return .init(CKUserIdentity.self, bufferingPolicy: .unbounded) { continuation in + let operation = CKDiscoverAllUserIdentitiesOperation() + operation.configuration.isLongLived = false + operation.configuration.allowsCellularAccess = true + operation.configuration.qualityOfService = .userInitiated + operation.userIdentityDiscoveredBlock = { + continuation.yield($0) + } + operation.discoverAllUserIdentitiesResultBlock = { + switch $0 { + case .success: + continuation.finish() + case let .failure(error): + continuation.finish(throwing: error) + } + } + add(operation) + } + } + + func discoverUserIdentities( + _ userIdentityLookupInfos: [CKUserIdentity.LookupInfo] + ) -> AsyncThrowingStream<(CKUserIdentity, CKUserIdentity.LookupInfo), Error> { + return AsyncThrowingStream<(CKUserIdentity, CKUserIdentity.LookupInfo), Error>((CKUserIdentity, CKUserIdentity.LookupInfo).self, bufferingPolicy: .unbounded) { continuation in + let operation = CKDiscoverUserIdentitiesOperation(userIdentityLookupInfos: userIdentityLookupInfos) + operation.configuration.isLongLived = false + operation.configuration.allowsCellularAccess = true + operation.configuration.qualityOfService = .userInitiated + operation.userIdentityDiscoveredBlock = { + continuation.yield(($0, $1)) + } + operation.discoverUserIdentitiesResultBlock = { + switch $0 { + case .success: + continuation.finish() + case let .failure(error): + continuation.finish(throwing: error) + } + } + add(operation) + } + } + + func fetchShareParticipants( + _ userIdentityLookupInfos: [CKUserIdentity.LookupInfo] + ) -> AsyncThrowingStream<(CKUserIdentity.LookupInfo, CKShare.Participant), Error> { + return AsyncThrowingStream<(CKUserIdentity.LookupInfo, CKShare.Participant), Error>((CKUserIdentity.LookupInfo, CKShare.Participant).self, bufferingPolicy: .unbounded) { continuation in + let operation = CKFetchShareParticipantsOperation(userIdentityLookupInfos: userIdentityLookupInfos) + operation.configuration.isLongLived = false + operation.configuration.allowsCellularAccess = true + operation.configuration.qualityOfService = .userInitiated + operation.perShareParticipantResultBlock = { + switch $1 { + case let .success(value): + continuation.yield(($0, value)) + case let .failure(error): + continuation.finish(throwing: error) + } + } + operation.fetchShareParticipantsResultBlock = { + switch $0 { + case .success: + continuation.finish() + case let .failure(error): + continuation.finish(throwing: error) + } + } + add(operation) + } + } + + func fetchShareParticipant( + _ userIdentity: CKUserIdentity.LookupInfo + ) async throws -> CKShare.Participant { + let stream = fetchShareParticipants([userIdentity]) + guard let foundUser = try await stream.first(where: { $0.0 == userIdentity })?.1 else { + assertionFailure("Expected a participant") + throw CKError(.internalError) + } + return foundUser + } + + /// An operation that fetches shared record metadata for one or more shares. + func fetchShareMetadata( + for shareURLs: [URL], + shouldFetchRootRecord: Bool = false + ) -> AsyncThrowingStream<(URL, CKShare.Metadata), Error> { + return AsyncThrowingStream<(URL, CKShare.Metadata), Error>.init((URL, CKShare.Metadata).self, bufferingPolicy: .unbounded) { continuation in + let operation = CKFetchShareMetadataOperation(shareURLs: shareURLs) + operation.configuration.isLongLived = false + operation.configuration.allowsCellularAccess = true + operation.configuration.qualityOfService = .userInitiated + operation.shouldFetchRootRecord = shouldFetchRootRecord + operation.perShareMetadataResultBlock = { + switch $1 { + case let .success(value): + continuation.yield(($0, value)) + case let .failure(error): + continuation.finish(throwing: error) + } + } + operation.fetchShareMetadataResultBlock = { + switch $0 { + case .success: + continuation.finish() + case let .failure(error): + continuation.finish(throwing: error) + } + } + add(operation) + } + } + + func acceptShares(_ shares: [CKShare.Metadata]) -> AsyncThrowingStream<(CKShare.Metadata, CKShare), Error> { + return AsyncThrowingStream<(CKShare.Metadata, CKShare), Error>.init((CKShare.Metadata, CKShare).self, bufferingPolicy: .unbounded) { continuation in + let operation = CKAcceptSharesOperation(shareMetadatas: shares) + operation.configuration.isLongLived = false + operation.configuration.allowsCellularAccess = true + operation.configuration.qualityOfService = .userInitiated + operation.perShareResultBlock = { + switch $1 { + case let .success(value): + continuation.yield(($0, value)) + case let .failure(error): + continuation.finish(throwing: error) + } + } + operation.acceptSharesResultBlock = { + switch $0 { + case .success: + continuation.finish() + case let .failure(error): + continuation.finish(throwing: error) + } + } + add(operation) + } + } +} + +internal extension CKDatabase { + + @discardableResult + func fetch(_ operation: CKFetchRecordsOperation) -> AsyncThrowingStream { + operation.configuration.isLongLived = false + operation.configuration.allowsCellularAccess = true + operation.configuration.qualityOfService = .userInitiated + return AsyncThrowingStream(CKRecord.self, bufferingPolicy: .unbounded) { continuation in + operation.perRecordResultBlock = { + switch $1 { + case let .success(value): + continuation.yield(value) + case let .failure(error): + continuation.finish(throwing: error) + } + } + operation.fetchRecordsResultBlock = { + switch $0 { + case .success: + continuation.finish() + case let .failure(error): + continuation.finish(throwing: error) + } + } + add(operation) + } + } + + func modify(_ operation: CKModifyRecordsOperation) async throws { + operation.configuration.isLongLived = false + operation.configuration.allowsCellularAccess = true + operation.configuration.qualityOfService = .userInitiated + return try await withCheckedThrowingContinuation { continuation in + operation.modifyRecordsResultBlock = { + continuation.resume(with: $0) + } + add(operation) + return + } + } + + func query( + _ operation: CKQueryOperation + ) -> AsyncThrowingStream { + operation.configuration.isLongLived = false + operation.configuration.allowsCellularAccess = true + operation.configuration.qualityOfService = .userInitiated + fatalError() + /* + return AsyncThrowingStream(CKQueryOperation.AsyncStreamValue.self, bufferingPolicy: .unbounded) { (continuation: AsyncThrowingStream.Continuation) in + + operation.recordMatchedBlock = { (id, result) in + switch result { + case let .success(value): + continuation.yield(.record(value)) + case let .failure(error): + continuation.finish(throwing: error) + } + } + operation.queryResultBlock { (result) in + switch result { + case let .success(value): + if let cursor = value { + continuation.yield(.cursor(value)) + } + continuation.finish() + case let .failure(error): + continuation.finish(throwing: error) + } + } + add(operation) + }*/ + } + + func queryAll( + _ query: CKQuery, + zone: CKRecordZone.ID? = nil + ) -> AsyncThrowingStream { + let operation = CKQueryOperation(query: query) + operation.zoneID = zone + operation.configuration.isLongLived = false + operation.configuration.allowsCellularAccess = true + operation.configuration.qualityOfService = .userInitiated + fatalError() + /* + return AsyncThrowingStream.init(CKRecord.self, bufferingPolicy: .unbounded) { continuation in + let task = Task.detached { + var cursor: CKQueryOperation.Cursor? + // first request + for try await value in self.query(operation) { + switch value { + case let .record(record): + continuation.yield(record) + case let .cursor(newCursor): + cursor = newCursor + } + } + // continue fetching if cursor returned + while let queryCursor = cursor { + let cursorOperation = CKQueryOperation(cursor: queryCursor) + cursorOperation.zoneID = zone + for try await value in self.query(operation) { + switch value { + case let .record(record): + continuation.yield(record) + case let .cursor(newCursor): + cursor = newCursor + } + } + } + } + continuation.onTermination = { + task.cancel() + } + }*/ + } + + func modifyZones( + save: [CKRecordZone]?, + delete: [CKRecordZone.ID]? = nil + ) async throws { + let operation = CKModifyRecordZonesOperation( + recordZonesToSave: save, + recordZoneIDsToDelete: delete + ) + operation.configuration.isLongLived = false + operation.configuration.allowsCellularAccess = true + operation.configuration.qualityOfService = .userInitiated + return try await withCheckedThrowingContinuation { continuation in + operation.modifyRecordZonesResultBlock = { + continuation.resume(with: $0) + } + add(operation) + } + } + + func fetchZones( + _ zones: [CKRecordZone.ID]? = nil + ) -> AsyncThrowingStream { + let operation = zones.flatMap { CKFetchRecordZonesOperation(recordZoneIDs: $0) } + ?? .fetchAllRecordZonesOperation() + operation.configuration.isLongLived = false + operation.configuration.allowsCellularAccess = true + operation.configuration.qualityOfService = .userInitiated + return .init(CKRecordZone.self, bufferingPolicy: .unbounded) { continuation in + operation.perRecordZoneResultBlock = { + switch $1 { + case let .success(value): + continuation.yield(value) + case let .failure(error): + continuation.finish(throwing: error) + } + } + operation.fetchRecordZonesResultBlock = { + switch $0 { + case .success: + continuation.finish() + case let .failure(error): + continuation.finish(throwing: error) + } + } + add(operation) + } + } + + func fetchZone(_ zone: CKRecordZone.ID) async throws -> CKRecordZone { + guard let zone = try await fetchZones([zone]).first(where: { $0.zoneID == zone }) else { + assertionFailure("Expected a matching zone") + throw CKError(.internalError) + } + return zone + } + + func modify( + subscriptions save: [CKSubscription]?, + delete: [CKSubscription.ID]? = nil + ) async throws { + let operation = CKModifySubscriptionsOperation( + subscriptionsToSave: save, + subscriptionIDsToDelete: delete + ) + operation.configuration.isLongLived = false + operation.configuration.allowsCellularAccess = true + operation.configuration.qualityOfService = .userInitiated + return try await withCheckedThrowingContinuation { continuation in + operation.modifySubscriptionsResultBlock = { + continuation.resume(with: $0) + } + add(operation) + } + } +} + +internal extension CKQueryOperation { + + enum AsyncStreamValue { + case record(CKRecord) + case cursor(CKQueryOperation.Cursor) + } +} diff --git a/Xcode/LockKit/Model/iCloud/CloudLock.swift b/Xcode/LockKit/Model/iCloud/CloudLock.swift new file mode 100644 index 00000000..dad328e5 --- /dev/null +++ b/Xcode/LockKit/Model/iCloud/CloudLock.swift @@ -0,0 +1,84 @@ +// +// LockCloud.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/14/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreData +import CloudKit +import CloudKitCodable +import CoreLock + +/// CloudKit Lock +public struct CloudLock { + + public let id: ID + + public var name: String +} + +public extension CloudLock { + + init?(managedObject: LockManagedObject) { + + guard let identifier = managedObject.identifier, + let name = managedObject.name + else { return nil } + + self.id = .init(rawValue: identifier) + self.name = name + } +} + +public extension CloudLock { + struct ID: RawRepresentable, Codable, Equatable, Hashable { + public let rawValue: UUID + public init(rawValue: UUID = UUID()) { + self.rawValue = rawValue + } + } +} + +extension CloudLock: CloudKitCodable { + public var cloudIdentifier: CloudKitIdentifier { + return id + } +} + +extension CloudLock.ID: CloudKitIdentifier { + + public static var cloudRecordType: CKRecord.RecordType { + return "Lock" + } + + public init?(cloudRecordID: CKRecord.ID) { + let string = cloudRecordID.recordName + .replacingOccurrences(of: type(of: self).cloudRecordType + "/", with: "") + guard let rawValue = UUID(uuidString: string) + else { return nil } + self.init(rawValue: rawValue) + } + + public var cloudRecordID: CKRecord.ID { + return CKRecord.ID(recordName: type(of: self).cloudRecordType + "/" + rawValue.uuidString) + } +} + +// MARK: - CloudKit Fetch + +public extension CloudStore { + + func fetchLocks() -> AsyncThrowingMapSequence, CloudLock> { + let database = container.privateCloudDatabase + let decoder = CloudKitDecoder(context: database) + let query = CKQuery( + recordType: CloudLock.ID.cloudRecordType, + predicate: NSPredicate(value: true) + ) + return database.queryAll(query) + .map { try decoder.decode(CloudLock.self, from: $0) } + } +} diff --git a/Xcode/LockKit/Model/iCloud/CloudNewKey.swift b/Xcode/LockKit/Model/iCloud/CloudNewKey.swift new file mode 100644 index 00000000..6706cffa --- /dev/null +++ b/Xcode/LockKit/Model/iCloud/CloudNewKey.swift @@ -0,0 +1,143 @@ +// +// CloudNewKey.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/14/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreLock +import CloudKit +import CloudKitCodable + +public extension NewKey { + + struct Cloud: Codable, Equatable { + + public let id: ID + + /// Lock this key belongs to. + public let lock: CloudLock.ID + + public var name: String + + public let created: Date + + public let expiration: Date + + public let permissionType: PermissionType + + public let schedule: Permission.Schedule.Cloud? + } +} + +public extension NewKey.Cloud { + + init(_ value: NewKey, lock: UUID) { + self.id = .init(rawValue: value.id) + self.lock = .init(rawValue: lock) + self.name = value.name + self.created = value.created + self.expiration = value.expiration + self.permissionType = value.permission.type + if case let .scheduled(schedule) = value.permission { + self.schedule = Permission.Schedule.Cloud(schedule, key: value.id, type: .newKey) + } else { + self.schedule = nil + } + } +} + +public extension NewKey { + + init?(_ cloud: Cloud) { + + let id = cloud.id.rawValue + let permission: Permission + + switch cloud.permissionType { + case .owner: + permission = .owner + case .admin: + permission = .admin + case .anytime: + permission = .anytime + case .scheduled: + guard let cloudSchedule = cloud.schedule, + let schedule = Permission.Schedule(cloudSchedule) + else { return nil } + permission = .scheduled(schedule) + } + + self.init( + id: id, + name: cloud.name, + permission: permission, + created: cloud.created, + expiration: cloud.expiration + ) + } +} + +public extension NewKey.Cloud { + struct ID: RawRepresentable, Codable, Equatable, Hashable { + public let rawValue: UUID + public init(rawValue: UUID = UUID()) { + self.rawValue = rawValue + } + } +} + +extension NewKey.Cloud: CloudKitCodable { + public var cloudIdentifier: CloudKitIdentifier { + return id + } + public var parentRecord: CloudKitIdentifier? { + return lock + } +} + +extension NewKey.Cloud.ID: CloudKitIdentifier { + + public static var cloudRecordType: CKRecord.RecordType { + return "NewKey" + } + + public init?(cloudRecordID: CKRecord.ID) { + let string = cloudRecordID.recordName + .replacingOccurrences(of: type(of: self).cloudRecordType + "/", with: "") + guard let rawValue = UUID(uuidString: string) + else { return nil } + self.init(rawValue: rawValue) + } + + public var cloudRecordID: CKRecord.ID { + return CKRecord.ID(recordName: type(of: self).cloudRecordType + "/" + rawValue.uuidString) + } +} + +// MARK: - CloudKit Fetch + +public extension CloudStore { + + func fetchNewKeys( + for lock: CloudLock.ID + ) -> AsyncThrowingMapSequence, NewKey.Cloud> { + let database = container.privateCloudDatabase + let decoder = CloudKitDecoder(context: database) + let lockReference = CKRecord.Reference( + recordID: lock.cloudRecordID, + action: .none + ) + let query = CKQuery( + recordType: NewKey.Cloud.ID.cloudRecordType, + predicate: NSPredicate(format: "%K == %@", "lock", lockReference) + ) + query.sortDescriptors = [ + .init(key: "created", ascending: false) // \Key.Cloud.created + ] + return database.queryAll(query) + .map { try decoder.decode(NewKey.Cloud.self, from: $0) } + } +} diff --git a/Xcode/LockKit/Model/iCloud/CloudNewKeyInvitation.swift b/Xcode/LockKit/Model/iCloud/CloudNewKeyInvitation.swift new file mode 100644 index 00000000..f59ef8b1 --- /dev/null +++ b/Xcode/LockKit/Model/iCloud/CloudNewKeyInvitation.swift @@ -0,0 +1,95 @@ +// +// NewKeyInvitation.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/20/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CloudKit +import CloudKitCodable +import CoreLock + +public extension NewKey.Invitation { + + struct Cloud: Codable, Equatable { + + public let id: ID + + /// Identifier of lock. + public let lock: UUID + + /// New Key to create. + public let key: Data // JSON Data + + /// Temporary shared secret to accept the key invitation. + public let secret: KeyData + } +} + +internal extension NewKey.Invitation.Cloud { + + static let keyEncoder = JSONEncoder() + + static let keyDecoder = JSONDecoder() +} + +internal extension NewKey.Invitation.Cloud { + + init(_ value: NewKey.Invitation) { + + self.id = .init(rawValue: value.key.id) + self.lock = value.lock + self.secret = value.secret + self.key = try! NewKey.Invitation.Cloud.keyEncoder.encode(value.key) + } +} + +internal extension NewKey.Invitation { + + init?(_ cloud: NewKey.Invitation.Cloud) { + + guard let key = try? NewKey.Invitation.Cloud.keyDecoder.decode(NewKey.self, from: cloud.key) + else { return nil } + + self.init(lock: cloud.lock, key: key, secret: cloud.secret) + } +} + +public extension NewKey.Invitation.Cloud { + struct ID: RawRepresentable, Codable, Equatable, Hashable { + public let rawValue: UUID + public init(rawValue: UUID) { + self.rawValue = rawValue + } + } +} + +extension NewKey.Invitation.Cloud: CloudKitCodable { + public var cloudIdentifier: CloudKitIdentifier { + return id + } + public var parentRecord: CloudKitIdentifier? { + return nil + } +} + +extension NewKey.Invitation.Cloud.ID: CloudKitIdentifier { + + public static var cloudRecordType: CKRecord.RecordType { + return "NewKeyInvitation" + } + + public init?(cloudRecordID: CKRecord.ID) { + let string = cloudRecordID.recordName + .replacingOccurrences(of: type(of: self).cloudRecordType + "/", with: "") + guard let rawValue = UUID(uuidString: string) + else { return nil } + self.init(rawValue: rawValue) + } + + public var cloudRecordID: CKRecord.ID { + return CKRecord.ID(recordName: type(of: self).cloudRecordType + "/" + rawValue.uuidString, zoneID: .lockShared) + } +} diff --git a/Xcode/LockKit/Model/iCloud/CloudPermission.swift b/Xcode/LockKit/Model/iCloud/CloudPermission.swift new file mode 100644 index 00000000..3eaab2fa --- /dev/null +++ b/Xcode/LockKit/Model/iCloud/CloudPermission.swift @@ -0,0 +1,187 @@ +// +// CloudPermission.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/19/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CloudKit +import CloudKitCodable +import CoreLock + +public extension Permission.Schedule { + + struct Cloud: Codable, Equatable { + + /// The unique identifier. + public let id: ID + + /// The date this permission becomes invalid. + public var expiry: Date? + + // The minute interval range the lock can be unlocked. + public var intervalMin: UInt16 + public var intervalMax: UInt16 + + // weekdays + public var sunday: Bool + public var monday: Bool + public var tuesday: Bool + public var wednesday: Bool + public var thursday: Bool + public var friday: Bool + public var saturday: Bool + } +} + +internal extension Permission.Schedule.Cloud { + + init(_ value: Permission.Schedule, key: UUID, type: KeyType) { + self.id = .init(key: key, type: type) + self.expiry = value.expiry + self.intervalMin = value.interval.rawValue.lowerBound + self.intervalMax = value.interval.rawValue.upperBound + self.sunday = value.weekdays.sunday + self.monday = value.weekdays.monday + self.tuesday = value.weekdays.tuesday + self.wednesday = value.weekdays.wednesday + self.thursday = value.weekdays.thursday + self.friday = value.weekdays.friday + self.saturday = value.weekdays.saturday + } +} + +internal extension Permission.Schedule { + + init?(_ cloud: Cloud) { + + guard let interval = Interval(rawValue: cloud.intervalMin ... cloud.intervalMax) + else { return nil } + + let weekdays = Weekdays( + sunday: cloud.sunday, + monday: cloud.monday, + tuesday: cloud.tuesday, + wednesday: cloud.wednesday, + thursday: cloud.thursday, + friday: cloud.friday, + saturday: cloud.saturday + ) + + self.init( + expiry: cloud.expiry, + interval: interval, + weekdays: weekdays + ) + } +} + +public extension Permission.Schedule.Cloud { + + /// Identifier + enum ID: Equatable, Hashable { + case key(Key.Cloud.ID) + case newKey(NewKey.Cloud.ID) + } +} + +public extension Permission.Schedule.Cloud.ID { + + init(key: UUID, type: KeyType) { + switch type { + case .key: + self = .key(.init(rawValue: key)) + case .newKey: + self = .newKey(.init(rawValue: key)) + } + } + + var type: KeyType { + switch self { + case .key: return .key + case .newKey: return .newKey + } + } +} + +extension Permission.Schedule.Cloud.ID: RawRepresentable { + + public init?(rawValue: String) { + let components = rawValue.split(separator: "/") + guard components.count == 3, + let keyIdentifier = UUID(uuidString: String(components[1])), + String(components[2]) == Swift.type(of: self).cloudRecordType + else { return nil } + let type: KeyType + switch String(components[0]) { + case Key.Cloud.ID.cloudRecordType: + type = .key + case NewKey.Cloud.ID.cloudRecordType: + type = .newKey + default: + return nil + } + self.init(key: keyIdentifier, type: type) + } + + public var rawValue: String { + switch self { + case let .key(key): + return Key.Cloud.ID.cloudRecordType + + "/" + key.rawValue.uuidString + + "/" + Swift.type(of: self).cloudRecordType + case let .newKey(newKey): + return NewKey.Cloud.ID.cloudRecordType + + "/" + newKey.rawValue.uuidString + + "/" + Swift.type(of: self).cloudRecordType + } + } +} + +extension Permission.Schedule.Cloud.ID: Codable { + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(String.self) + guard let value = Permission.Schedule.Cloud.ID(rawValue: rawValue) else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid string value \(rawValue)") + } + self = value + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } +} + +extension Permission.Schedule.Cloud: CloudKitCodable { + public var cloudIdentifier: CloudKitIdentifier { + return id + } + public var parentRecord: CloudKitIdentifier? { + switch id { + case let .key(key): + return key + case let .newKey(newKey): + return newKey + } + } +} + +extension Permission.Schedule.Cloud.ID: CloudKitIdentifier { + + public static var cloudRecordType: CKRecord.RecordType { + return "PermissionSchedule" + } + + public init?(cloudRecordID: CKRecord.ID) { + self.init(rawValue: cloudRecordID.recordName) + } + + public var cloudRecordID: CKRecord.ID { + return CKRecord.ID(recordName: rawValue) + } +} diff --git a/Xcode/LockKit/Model/iCloud/CloudShare.swift b/Xcode/LockKit/Model/iCloud/CloudShare.swift new file mode 100644 index 00000000..6eaa0253 --- /dev/null +++ b/Xcode/LockKit/Model/iCloud/CloudShare.swift @@ -0,0 +1,233 @@ +// +// CloudShare.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/21/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CloudKit +import CloudKitCodable +import CoreLock + +#if os(iOS) +import UIKit +#endif + +public enum CloudShare { + + public enum ShareType: String { + + case newKey = "com.colemancda.Lock.CloudKit.Share.NewKey" + } + + public struct NewKey: Codable, Equatable { + + /// Identifier + public let id: ID + + /// New Key invitation share URL + public let invitation: URL + + /// User recieving the new key. + public let user: String + } +} + +public extension CloudShare.NewKey { + struct ID: RawRepresentable, Codable, Equatable, Hashable { + public let rawValue: UUID + public init(rawValue: UUID) { + self.rawValue = rawValue + } + } +} + +extension CloudShare.NewKey: CloudKitCodable { + public var cloudIdentifier: CloudKitIdentifier { + return id + } + public var parentRecord: CloudKitIdentifier? { + return nil + } +} + +extension CloudShare.NewKey.ID: CloudKitIdentifier { + + public static var cloudRecordType: CKRecord.RecordType { + return "NewKeyShare" + } + + public init?(cloudRecordID: CKRecord.ID) { + let string = cloudRecordID.recordName + .replacingOccurrences(of: type(of: self).cloudRecordType + "/", with: "") + guard let rawValue = UUID(uuidString: string) + else { return nil } + self.init(rawValue: rawValue) + } + + public var cloudRecordID: CKRecord.ID { + return CKRecord.ID(recordName: type(of: self).cloudRecordType + "/" + rawValue.uuidString) + } +} + +// MARK: - CloudKit Fetch + +internal extension CloudStore { + + func fetchNewKeyPublicShares( + for user: CKRecord.ID? = nil + ) async throws -> AsyncThrowingMapSequence, CloudShare.NewKey> { + let database = container.publicCloudDatabase + let decoder = CloudKitDecoder(context: database) + let userID: CKRecord.ID + if let id = user { + userID = id + } else { + userID = try await container.fetchUserRecordID() + } + let query = CKQuery( + recordType: CloudShare.NewKey.ID.cloudRecordType, + predicate: NSPredicate(format: "%K == %@", "user", userID.recordName) + ) + return database.queryAll(query) + .map { try decoder.decode(CloudShare.NewKey.self, from: $0) } + } +} + +// MARK: - CloudKit Subscriptions + +public extension CloudStore { + + func subcribeNewKeyShares() async throws { + let user = try await container.fetchUserRecordID() + let subcription = CKQuerySubscription( + recordType: CloudShare.NewKey.ID.cloudRecordType, + predicate: NSPredicate(format: "%K == %@", "user", user.recordName), + options: [.firesOnRecordCreation] + ) + let notificationInfo = CKQuerySubscription.NotificationInfo() + notificationInfo.shouldSendContentAvailable = true + subcription.notificationInfo = notificationInfo + try await container.publicCloudDatabase.modify(subscriptions: [subcription]) + } +} + +// MARK: - CloudKit Zone + +public extension CKRecordZone.ID { + + static var lockShared: CKRecordZone.ID { + return .init(zoneName: "shared", ownerName: CKCurrentUserDefaultName) + } +} + +// MARK: - CloudKit Sharing + +public extension CloudStore { + + func share( + _ invitation: NewKey.Invitation, + to user: CloudUser.ID + ) async throws { + + // make sure zone is created + let zone = CKRecordZone(zoneID: .lockShared) + try await container.privateCloudDatabase.modifyZones(save: [zone]) + + // save invitation + let cloudInvitation = NewKey.Invitation.Cloud(invitation) + let privateCloudEncoder = CloudKitEncoder(context: container.privateCloudDatabase) + let operation = try privateCloudEncoder.encode(cloudInvitation) + guard let invitationRecord = operation.recordsToSave?.first + else { fatalError() } + assert(cloudInvitation.cloudIdentifier.cloudRecordID == invitationRecord.recordID) + assert(type(of: cloudInvitation.cloudIdentifier).cloudRecordType == invitationRecord.recordType) + operation.isAtomic = true + operation.savePolicy = .allKeys + + // create private data share + let invitationShare = CKShare( + rootRecord: invitationRecord, + shareID: CKRecord.ID(recordName: UUID().uuidString, zoneID: .lockShared) + ) + invitationShare.publicPermission = .none + invitationShare[CKShare.SystemFieldKey.title] = "New \(invitation.key.permission.type.localizedText) key" + #if os(iOS) + //invitationShare[CKShare.SystemFieldKey.thumbnailImageData] = UIImage(permission: invitation.key.permission).pngData() + #endif + invitationShare[CKShare.SystemFieldKey.shareType] = CloudShare.ShareType.newKey.rawValue + + // add shared user + let participant = try await container.fetchShareParticipant(.init(userRecordID: user.cloudRecordID)) + participant.permission = .readWrite + invitationShare.addParticipant(participant) + + // upload share + operation.recordsToSave?.append(invitationShare) + try await container.privateCloudDatabase.modify(operation) + guard let shareURL = invitationShare.url else { + assertionFailure("Missing CloudKit share URL") + throw CKError(.internalError) + } + + // upload public share data with invitation url + let publicShare = CloudShare.NewKey( + id: .init(rawValue: invitation.key.id), + invitation: shareURL, + user: user.cloudRecordID.recordName + ) + + try await upload(publicShare, database: .public) + } + + func fetchNewKeyShares( + invitations: ([NewKey.Invitation]) async throws -> () + ) async throws { + + // fetch public shares + let publicShares = try await fetchNewKeyPublicShares() + .reduce(into: [CloudShare.NewKey](), { $0.append($1) }) + guard publicShares.isEmpty == false else { return } + + // accept pending shares + let shareURLs = publicShares.map { $0.invitation } + let metadata = try await container.fetchShareMetadata(for: shareURLs, shouldFetchRootRecord: true) + .reduce(into: [CKShare.Metadata](), { $0.append($1.1) }) + assert(metadata.count == publicShares.count) + let pendingShares = metadata.filter { $0.participantStatus == .pending } + if pendingShares.isEmpty == false { + let _ = try await container.acceptShares(pendingShares) + .reduce(into: [], { $0.append($1) }) + } + + let sharedRecords = metadata.compactMap { $0.rootRecord } + assert(sharedRecords.count == metadata.count) + + let sharedDecoder = CloudKitDecoder(context: container.sharedCloudDatabase) + let sharedInvitations = try sharedRecords + .map { try sharedDecoder.decode(NewKey.Invitation.Cloud.self, from: $0) } + .compactMap { NewKey.Invitation($0) } + assert(sharedInvitations.count == sharedRecords.count) + + // handle invitations + try await invitations(sharedInvitations) // won't delete if error is thrown + + // delete shares + let deleteSharesOperation = CKModifyRecordsOperation( + recordsToSave: [], + recordIDsToDelete: metadata.map { $0.share.recordID } + ) + deleteSharesOperation.isAtomic = true + try await container.sharedCloudDatabase.modify(deleteSharesOperation) + + // delete public share data + let deletePublicSharesOperation = CKModifyRecordsOperation( + recordsToSave: [], + recordIDsToDelete: publicShares.map { $0.id.cloudRecordID } + ) + deletePublicSharesOperation.isAtomic = true + try await container.publicCloudDatabase.modify(deletePublicSharesOperation) + } +} diff --git a/Xcode/LockKit/Model/iCloud/CloudUser.swift b/Xcode/LockKit/Model/iCloud/CloudUser.swift new file mode 100644 index 00000000..f94827fd --- /dev/null +++ b/Xcode/LockKit/Model/iCloud/CloudUser.swift @@ -0,0 +1,72 @@ +// +// CloudUser.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/13/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CloudKit +import CloudKitCodable + +/// CloudKit Lock User +public struct CloudUser: Codable, Equatable { + + public let id: ID + + // Should only be on private DB + public var applicationData: ApplicationData.Cloud? + + /// Avatar image + //public var avatar: URL? + + /// Full name + //public var name: String? +} + +public extension CloudUser { + + static func fetch( + in container: CKContainer = .lock, + database scope: CKDatabase.Scope = .private + ) async throws -> CloudUser { + let recordID = try await container.fetchUserRecordID() + let database = container.database(with: scope) + guard let record = try database.fetch(record: recordID) else { + throw CKError(.unknownItem) // a user should always exist + } + let decoder = CloudKitDecoder(context: database) + return try decoder.decode(self, from: record) + } +} + +public extension CloudUser { + struct ID: RawRepresentable, Codable, Equatable, Hashable { + public let rawValue: String + public init(rawValue: String) { + self.rawValue = rawValue + } + } +} + +extension CloudUser: CloudKitCodable { + public var cloudIdentifier: CloudKitIdentifier { + return id + } +} + +extension CloudUser.ID: CloudKitIdentifier { + + public static var cloudRecordType: CKRecord.RecordType { + return CKRecord.SystemType.userRecord + } + + public init?(cloudRecordID: CKRecord.ID) { + self.init(rawValue: cloudRecordID.recordName) + } + + public var cloudRecordID: CKRecord.ID { + return CKRecord.ID(recordName: rawValue) + } +} diff --git a/Xcode/LockKit/Model/iCloud/iCloud.swift b/Xcode/LockKit/Model/iCloud/iCloud.swift new file mode 100644 index 00000000..39aabb5b --- /dev/null +++ b/Xcode/LockKit/Model/iCloud/iCloud.swift @@ -0,0 +1,556 @@ +// +// iCloud.swift +// LockKit +// +// Created by Alsey Coleman Miller on 8/25/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreData +import CloudKit +import CloudKitCodable +import KeychainAccess +import CoreLock + +public final class CloudStore { + + public static let shared = CloudStore() + + deinit { + + #if os(iOS) + if let observer = keyValueStoreObserver { + NotificationCenter.default.removeObserver(observer) + } + #endif + } + + private init() { + + #if os(iOS) + // observe changes + keyValueStoreObserver = NotificationCenter.default.addObserver( + forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification, + object: self.keyValueStore, + queue: nil, + using: { [weak self] in self?.didChangeExternally($0) }) + #endif + } + + // MARK: - Properties + + public var didChange: (() -> ())? + + private lazy var keychain = Keychain(service: .lockCloud, accessGroup: .lock).synchronizable(true) + + #if os(iOS) + private lazy var keyValueStore: NSUbiquitousKeyValueStore = .default + #endif + + private var keyValueStoreObserver: NSObjectProtocol? + + internal lazy var container: CKContainer = .lock + + // MARK: - Methods + + @discardableResult + public func requestPermissions() async throws -> CKContainer.ApplicationPermissionStatus { + return try await container.requestApplicationPermission([.userDiscoverability]) + } + + public func accountStatus() async throws -> CKAccountStatus { + return try await container.accountStatus() + } + + public func upload( + applicationData: ApplicationData, + keys: [UUID: KeyData] + ) async throws { + + // store lock private keys in iCloud keychain + for (keyIdentifier, keyData) in keys { + assert(applicationData[key: keyIdentifier] != nil, "Invalid key") + try keychain.set(keyData.data, key: keyIdentifier.uuidString) + } + + // update iCloud user + var user = try await CloudUser.fetch(in: container, database: .private) + user.applicationData = .init(applicationData, user: user.id) // set new application data + try await upload(user) + + // inform via key value store + didUpload(applicationData: applicationData) + } + + @MainActor + public func upload(context: NSManagedObjectContext) async throws { + assert(Thread.isMainThread) + // upload all locks and keys + let locks = try LockManagedObject.fetch(in: context) + for lockManagedObject in locks { + assert(Thread.isMainThread) + guard let lock = CloudLock(managedObject: lockManagedObject) else { + assertionFailure("Invalid \(lockManagedObject)") + continue + } + try await upload(lock) + // upload events + let events = ((lockManagedObject.events as? Set) ?? []) + .lazy + .sorted(by: { $0.date! > $1.date! }) + for eventManagedObject in events { + guard let event = LockEvent(managedObject: eventManagedObject) + .flatMap({ LockEvent.Cloud(event: $0, for: lock.id.rawValue) }) else { + assertionFailure("Invalid \(eventManagedObject)") + continue + } + if try container.privateCloudDatabase.fetch(record: event.cloudIdentifier.cloudRecordID) == nil { + let _ = try await upload(event) + } else { + break // don't upload older events + } + } + // upload keys + let keys = ((lockManagedObject.keys as? Set) ?? []) + .lazy + .sorted(by: { $0.created! > $1.created! }) + for managedObject in keys { + guard let key = Key(managedObject: managedObject) + .flatMap({ Key.Cloud($0, lock: lock.id.rawValue) }) else { + assertionFailure("Invalid \(managedObject)") + continue + } + if try container.privateCloudDatabase.fetch(record: key.cloudIdentifier.cloudRecordID) == nil { + let _ = try await upload(key) + } else { + break // don't upload older events + } + } + // upload new keys + let newKeys = ((lockManagedObject.pendingKeys as? Set) ?? []) + .lazy + .sorted(by: { $0.created! > $1.created! }) + for managedObject in newKeys { + guard let newKey = NewKey(managedObject: managedObject) + .flatMap({ NewKey.Cloud($0, lock: lock.id.rawValue) }) else { + assertionFailure("Invalid \(managedObject)") + continue + } + let existingRecord = try container.privateCloudDatabase.fetch(record: newKey.cloudIdentifier.cloudRecordID) + if existingRecord == nil { + let _ = try await upload(newKey) + } else { + break // don't upload older events + } + } + } + } + + @discardableResult + internal func upload ( + _ encodable: T, database + scope: CKDatabase.Scope = .private + ) async throws -> CKRecord { + let database = container.database(with: scope) + let cloudEncoder = CloudKitEncoder(context: database) + let operation = try cloudEncoder.encode(encodable) + guard let record = operation.recordsToSave?.first + else { fatalError() } + assert(encodable.cloudIdentifier.cloudRecordID == record.recordID) + assert(type(of: encodable.cloudIdentifier).cloudRecordType == record.recordType) + operation.isAtomic = true + operation.savePolicy = .changedKeys + try await database.modify(operation) + return record + } + + public func downloadApplicationData() async throws -> (applicationData: ApplicationData, keys: [UUID: KeyData])? { + + // get iCloud user + let user = try await CloudUser.fetch(in: container, database: .private) + guard let cloudData = user.applicationData + else { return nil } + guard let applicationData = ApplicationData(cloudData) else { + #if DEBUG + dump(cloudData) + assertionFailure("Could not initialize from iCloud") + #endif + throw CKError(.internalError) + } + // download keys from keychain + var keys = [UUID: KeyData](minimumCapacity: applicationData.locks.count) + for key in applicationData.keys { + guard let data = try keychain.getData(key.id.uuidString), + let keyData = KeyData(data: data) + else { throw Error.missingKeychainItem(key.id) } + keys[key.id] = keyData + } + + return (applicationData, keys) + } + + private func didUpload(applicationData: ApplicationData) { + + #if os(iOS) + // inform iCloud Key Value Store + keyValueStore.set(applicationData.updated as NSDate, forKey: UbiquitousKey.updated.rawValue) + keyValueStore.synchronize() + #elseif os(watchOS) + + #endif + } + + #if os(iOS) + public func lastUpdated() -> Date? { + + keyValueStore.synchronize() + return keyValueStore.object(forKey: UbiquitousKey.updated.rawValue) as? Date + } + #endif + + #if os(iOS) + private func didChangeExternally(_ notification: Notification) { + + keyValueStore.synchronize() + didChange?() + } + #endif +} + +public extension CloudStore { + + /// CloudStore Error + enum Error: Swift.Error { + + /// Could not import due to missing KeyChain item. + case missingKeychainItem(UUID) + } +} + +private extension CloudStore { + + enum KeyChainKey: String { + + case applicationData = "com.colemancda.Lock.ApplicationData" + } +} + +private extension Keychain { + + func set(_ value: Data, key: CloudStore.KeyChainKey) throws { + try set(value, key: key.rawValue) + } + + func getData(_ key: CloudStore.KeyChainKey) throws -> Data? { + return try getData(key.rawValue) + } +} + +private extension CloudStore { + + enum UbiquitousKey: String { + + case updated + } +} + +internal extension ApplicationData { + + /// Attempt to update with no conflicts. + func update(with applicationData: ApplicationData) -> ApplicationData? { + + // must be originally the same application data + guard self.id == applicationData.id, + self.created == applicationData.created + else { return nil } + + // if local copy is newer, should not be overwritten with older copy. + guard self.locks != applicationData.locks else { + // locks not changed + if self.updated <= applicationData.updated { + return applicationData + } else { + return self + } + } + + // if local copy is newer, should not be overwritten with older copy. + guard self.keys != applicationData.keys else { + // no keys changed, keep newer local copy + if self.updated <= applicationData.updated { + return applicationData + } else { + return self + } + } + + // overwrite with newer cloud data + guard self.updated > applicationData.updated + else { return applicationData } + + return nil + } +} + +@MainActor +public extension Store { + + #if os(iOS) + func cloudDidChangeExternally() { + + if let lastUpdatedCloud = self.cloud.lastUpdated() { + guard self.applicationData.updated != lastUpdatedCloud + else { return } + } + + log("☁️ iCloud changed externally") + /* + do { + try self.syncCloud(conflicts: { _ in + return nil + }) + } + catch { log("⚠️ Could not sync iCloud: \(error.localizedDescription)") }*/ + } + #endif + + func syncCloud( + conflicts: (ApplicationData) -> Bool? = { _ in return nil } + ) async throws { + + // make sure iCloud is enabled + guard preferences.isCloudBackupEnabled, + try await cloud.accountStatus() == .available + else { return } + + // download to CoreData + try await downloadCloudLocks() + + // local data should override remote + await updateCoreData() + + // upload from CoreData + try await uploadCloudLocks() + + // download and upload application data + if try await downloadCloudApplicationData(conflicts: conflicts) { + try await uploadCloudApplicationData() + } + + // update preferences + self.preferences.lastCloudUpdate = Date() + } + + func uploadCloudLocks() async throws { + // main thread + let context = persistentContainer.viewContext + try await self.cloud.upload(context: context) + } + + func downloadCloudLocks() async throws { + + log("☁️ Will download locks") + + var insertedEventsCount = 0 + var insertedKeysCount = 0 + var insertedNewKeysCount = 0 + + defer { + log("☁️ Downloaded locks") + if insertedEventsCount > 0 { + log("☁️ Fetched \(insertedEventsCount) events") + } + if insertedKeysCount > 0 { + log("☁️ Fetched \(insertedKeysCount) keys") + } + if insertedNewKeysCount > 0 { + log("☁️ Fetched \(insertedNewKeysCount) pending keys") + } + } + + let applicationData = self.applicationData + for try await lock in cloud.fetchLocks() { + + // get current cached data + let cache = applicationData.locks[lock.id.rawValue] + // store in CoreData + await backgroundContext.commit { (context) in + let managedObject = try context.insert(lock) + // override name + if let name = cache?.name { + managedObject.name = name + } + } + + // fetch events + for try await cloudEvent in cloud.fetchEvents(for: lock.id) { + guard let event = LockEvent(cloudEvent) else { + assertionFailure() + continue + } + // save event in CoreData + await backgroundContext.commit { + // if already in CoreData, then stop + guard try EventManagedObject.find(event.id, in: $0) == nil + else { return } + try $0.insert(event, for: cloudEvent.lock.rawValue) + insertedEventsCount += 1 + } + } + + // fetch keys + for try await cloudValue in cloud.fetchKeys(for: lock.id) { + guard let value = Key(cloudValue) else { + assertionFailure() + continue // more values, ignore invalid cloud value + } + await backgroundContext.commit { + // if already in CoreData, then stop + guard try $0.find(id: value.id, type: KeyManagedObject.self) == nil + else { return } + // save event in CoreData + try $0.insert(value, for: cloudValue.lock.rawValue) + insertedKeysCount += 1 + } + } + + // fetch new keys + for try await cloudValue in cloud.fetchNewKeys(for: lock.id) { + guard let value = NewKey(cloudValue) else { + assertionFailure() + continue // more values, ignore invalid cloud value + } + await backgroundContext.commit { + // if already in CoreData, then stop + guard try $0.find(id: value.id, type: NewKeyManagedObject.self) == nil + else { return } + // save event in CoreData + try $0.insert(value, for: cloudValue.lock.rawValue) + insertedNewKeysCount += 1 + } + } + } + } + + /// Attempts to downlad `ApplicationData` from iCloud and returns success merging. + @discardableResult + func downloadCloudApplicationData( + conflicts: (ApplicationData) -> Bool? + ) async throws -> Bool { + + guard let (cloudData, cloudKeys) = try await cloud.downloadApplicationData() else { + log("☁️ No data in iCloud") + return true + } + + // Import private keys + var newKeysCount = 0 + for (identifier, keyData) in cloudKeys { + if self[key: identifier] == nil { + self[key: identifier] = keyData + newKeysCount += 1 + } + } + if newKeysCount > 0 { + log("☁️ Imported \(newKeysCount) keys from iCloud") + } + + // Import application data + let oldApplicationData = self.applicationData + guard cloudData != oldApplicationData else { + log("☁️ No new data from iCloud") + return false + } + + #if DEBUG + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .short + dateFormatter.timeStyle = .medium + print("Cloud: \(cloudData.id) \(dateFormatter.string(from: cloudData.updated))") + dump(cloudData) + print("Local: \(oldApplicationData.id) \(dateFormatter.string(from: oldApplicationData.updated))") + dump(oldApplicationData) + #endif + + // attempt to overwrite + if let newData = oldApplicationData.update(with: cloudData) { + // write new application data + self.applicationData = newData + if newData != oldApplicationData { + log("☁️ Updated application data from iCloud") + } else { + log("☁️ Keeping local data over iCloud") + } + } else if let shouldOverwrite = conflicts(cloudData) { + // ask user to replace with conflicting data + if shouldOverwrite { + self.applicationData = cloudData + log("☁️ Overriding application data from iCloud") + } else { + log("☁️ Discarding conflicting iCloud application data") + } + } else { + log("☁️ Aborted iCloud download due to unresolved conflict") + return false + } + // remove old keys + var removedKeys = 0 + let newData = self.applicationData + let newKeys = newData.keys.map { $0.id } + let oldKeys = oldApplicationData.keys.map { $0.id } + for oldKey in oldKeys { + // old key no longer exists + guard newKeys.contains(oldKey) == false + else { continue } + // remove from keychain + self[key: oldKey] = nil + removedKeys += 1 + } + if removedKeys > 0 { + log("☁️ Removed \(removedKeys) old keys from keychain") + } + log("☁️ Downloaded application data from iCloud") + return true + } + + func uploadCloudApplicationData() async throws { + + let applicationData = self.applicationData + + // read from to keychain + var keys = [UUID: KeyData]() + for key in applicationData.keys { + let keyData = self[key: key.id] + keys[key.id] = keyData + } + + // upload keychain and application data to iCloud + try await cloud.upload(applicationData: applicationData, keys: keys) + + log("☁️ Uploaded application data to iCloud") + } +} + +// MARK: - CloudKit Extensions + +/// UbiquityContainerIdentifier +/// +/// iCloud Identifier +public enum UbiquityContainerIdentifier: String { + + case lock = "iCloud.com.colemancda.Lock" +} + +public extension FileManager { + + /** + Returns the URL for the iCloud container associated with the specified identifier and establishes access to that container. + + - Note: Do not call this method from your app’s main thread. Because this method might take a nontrivial amount of time to set up iCloud and return the requested URL, you should always call it from a secondary thread. + */ + func ubiquityContainerURL(for identifier: UbiquityContainerIdentifier) -> URL? { + assert(Thread.isMainThread == false, "Use iCloud from secondary thread") + return url(forUbiquityContainerIdentifier: identifier.rawValue) + } +} diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 5b4446b2..e888a9e9 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -46,6 +46,19 @@ 6E21834A28D90F7A00A622B3 /* LocalAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21834928D90F7A00A622B3 /* LocalAuthentication.swift */; }; 6E21834C28D9140D00A622B3 /* KeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21834B28D9140D00A622B3 /* KeysView.swift */; }; 6E21834E28D91FDC00A622B3 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21834D28D91FDC00A622B3 /* Event.swift */; }; + 6E21835028D9506100A622B3 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21834F28D9506100A622B3 /* Preferences.swift */; }; + 6E21835D28D9516A00A622B3 /* CloudShare.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21835228D9516A00A622B3 /* CloudShare.swift */; }; + 6E21835E28D9516B00A622B3 /* CloudKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21835328D9516A00A622B3 /* CloudKey.swift */; }; + 6E21835F28D9516B00A622B3 /* CloudApplicationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21835428D9516A00A622B3 /* CloudApplicationData.swift */; }; + 6E21836028D9516B00A622B3 /* CloudEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21835528D9516A00A622B3 /* CloudEvent.swift */; }; + 6E21836128D9516B00A622B3 /* CloudKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21835628D9516A00A622B3 /* CloudKit.swift */; }; + 6E21836228D9516B00A622B3 /* CloudPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21835728D9516A00A622B3 /* CloudPermission.swift */; }; + 6E21836328D9516B00A622B3 /* iCloud.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21835828D9516A00A622B3 /* iCloud.swift */; }; + 6E21836428D9516B00A622B3 /* CloudUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21835928D9516A00A622B3 /* CloudUser.swift */; }; + 6E21836528D9516B00A622B3 /* CloudLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21835A28D9516A00A622B3 /* CloudLock.swift */; }; + 6E21836628D9516B00A622B3 /* CloudNewKeyInvitation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21835B28D9516A00A622B3 /* CloudNewKeyInvitation.swift */; }; + 6E21836728D9516B00A622B3 /* CloudNewKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21835C28D9516A00A622B3 /* CloudNewKey.swift */; }; + 6E21836A28D953D000A622B3 /* CloudKitCodable in Frameworks */ = {isa = PBXBuildFile; productRef = 6E21836928D953D000A622B3 /* CloudKitCodable */; }; 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276BB28D708A000AF171B /* CoreLock */; }; 6E3276C328D70A3700AF171B /* HomeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E3276C228D70A3700AF171B /* HomeKit.framework */; }; 6E3276C628D70A3700AF171B /* RequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276C528D70A3700AF171B /* RequestHandler.swift */; }; @@ -134,6 +147,18 @@ 6E21834928D90F7A00A622B3 /* LocalAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthentication.swift; sourceTree = ""; }; 6E21834B28D9140D00A622B3 /* KeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeysView.swift; sourceTree = ""; }; 6E21834D28D91FDC00A622B3 /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; + 6E21834F28D9506100A622B3 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; + 6E21835228D9516A00A622B3 /* CloudShare.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudShare.swift; sourceTree = ""; }; + 6E21835328D9516A00A622B3 /* CloudKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudKey.swift; sourceTree = ""; }; + 6E21835428D9516A00A622B3 /* CloudApplicationData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudApplicationData.swift; sourceTree = ""; }; + 6E21835528D9516A00A622B3 /* CloudEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudEvent.swift; sourceTree = ""; }; + 6E21835628D9516A00A622B3 /* CloudKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudKit.swift; sourceTree = ""; }; + 6E21835728D9516A00A622B3 /* CloudPermission.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudPermission.swift; sourceTree = ""; }; + 6E21835828D9516A00A622B3 /* iCloud.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iCloud.swift; sourceTree = ""; }; + 6E21835928D9516A00A622B3 /* CloudUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudUser.swift; sourceTree = ""; }; + 6E21835A28D9516A00A622B3 /* CloudLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudLock.swift; sourceTree = ""; }; + 6E21835B28D9516A00A622B3 /* CloudNewKeyInvitation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudNewKeyInvitation.swift; sourceTree = ""; }; + 6E21835C28D9516A00A622B3 /* CloudNewKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudNewKey.swift; sourceTree = ""; }; 6E3276BA28D7088D00AF171B /* SmartLock */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SmartLock; path = ..; sourceTree = ""; }; 6E3276C128D70A3700AF171B /* MatterLock.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MatterLock.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6E3276C228D70A3700AF171B /* HomeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HomeKit.framework; path = Library/Frameworks/HomeKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -184,6 +209,7 @@ 6E3276D928D70FA000AF171B /* GATT in Frameworks */, 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */, 6E3276D728D70FA000AF171B /* DarwinGATT in Frameworks */, + 6E21836A28D953D000A622B3 /* CloudKitCodable in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -221,6 +247,24 @@ path = Extensions; sourceTree = ""; }; + 6E21835128D9516A00A622B3 /* iCloud */ = { + isa = PBXGroup; + children = ( + 6E21835228D9516A00A622B3 /* CloudShare.swift */, + 6E21835328D9516A00A622B3 /* CloudKey.swift */, + 6E21835428D9516A00A622B3 /* CloudApplicationData.swift */, + 6E21835528D9516A00A622B3 /* CloudEvent.swift */, + 6E21835628D9516A00A622B3 /* CloudKit.swift */, + 6E21835728D9516A00A622B3 /* CloudPermission.swift */, + 6E21835828D9516A00A622B3 /* iCloud.swift */, + 6E21835928D9516A00A622B3 /* CloudUser.swift */, + 6E21835A28D9516A00A622B3 /* CloudLock.swift */, + 6E21835B28D9516A00A622B3 /* CloudNewKeyInvitation.swift */, + 6E21835C28D9516A00A622B3 /* CloudNewKey.swift */, + ); + path = iCloud; + sourceTree = ""; + }; 6E3276B928D7088D00AF171B /* Packages */ = { isa = PBXGroup; children = ( @@ -263,12 +307,14 @@ 6E3276DA28D7136400AF171B /* Model */ = { isa = PBXGroup; children = ( + 6E21835128D9516A00A622B3 /* iCloud */, 6E21832428D8F3B500A622B3 /* CoreData */, 6E21830D28D7FF2400A622B3 /* AppGroup.swift */, 6E21831428D80FF900A622B3 /* ApplicationData.swift */, 6E3276DB28D7195400AF171B /* Central.swift */, 6E21830028D7C37500A622B3 /* Error.swift */, 6E21830B28D7FEF600A622B3 /* FileManager.swift */, + 6E21834F28D9506100A622B3 /* Preferences.swift */, 6E21831228D80FDD00A622B3 /* JSON.swift */, 6E21830F28D80DCD00A622B3 /* Keychain.swift */, 6E21831628D8116C00A622B3 /* NewKey.swift */, @@ -447,6 +493,7 @@ 6E3276D628D70FA000AF171B /* DarwinGATT */, 6E3276D828D70FA000AF171B /* GATT */, 6E21830928D7FEA900A622B3 /* KeychainAccess */, + 6E21836928D953D000A622B3 /* CloudKitCodable */, ); productName = LockKit; productReference = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; @@ -486,6 +533,7 @@ packageReferences = ( 6E3276D528D70FA000AF171B /* XCRemoteSwiftPackageReference "GATT" */, 6E21830828D7FEA900A622B3 /* XCRemoteSwiftPackageReference "KeychainAccess" */, + 6E21836828D953D000A622B3 /* XCRemoteSwiftPackageReference "CloudKitCodable" */, ); productRefGroup = 6EA7768228D7061600018FA3 /* Products */; projectDirPath = ""; @@ -555,23 +603,30 @@ files = ( 6E21832328D8D26300A622B3 /* LockDetailView.swift in Sources */, 6E4CB61F28D7902600116573 /* NSStyleKit.swift in Sources */, + 6E21835D28D9516A00A622B3 /* CloudShare.swift in Sources */, 6E21831928D8116C00A622B3 /* NewKeyDocument.swift in Sources */, 6ED248CC28D7A3BF00F78D07 /* ActivityIndicatorView.swift in Sources */, 6E21830528D7C51900A622B3 /* Log.swift in Sources */, 6E21833B28D8F3B500A622B3 /* ConfirmNewKeyEventManagedObject.swift in Sources */, 6E21830C28D7FEF600A622B3 /* FileManager.swift in Sources */, 6E4CB61028D7867700116573 /* LockRowView.swift in Sources */, + 6E21835028D9506100A622B3 /* Preferences.swift in Sources */, 6E21831828D8116C00A622B3 /* NewKey.swift in Sources */, + 6E21835F28D9516B00A622B3 /* CloudApplicationData.swift in Sources */, 6E21831028D80DCD00A622B3 /* Keychain.swift in Sources */, 6E21833C28D8F3B500A622B3 /* KeyManagedObject.swift in Sources */, 6E21830128D7C37500A622B3 /* Error.swift in Sources */, + 6E21836328D9516B00A622B3 /* iCloud.swift in Sources */, 6E21833E28D8F3B500A622B3 /* CreateNewKeyEventManagedObject.swift in Sources */, 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */, 6E21830728D7D08300A622B3 /* Permission.swift in Sources */, 6E21833F28D8F3B500A622B3 /* LockInformationManagedObject.swift in Sources */, 6E21833D28D8F3B500A622B3 /* EventManagedObject.swift in Sources */, + 6E21836628D9516B00A622B3 /* CloudNewKeyInvitation.swift in Sources */, 6E21834128D8F3B500A622B3 /* Model.xcdatamodeld in Sources */, 6E21833728D8F3B500A622B3 /* PersistentContainer.swift in Sources */, + 6E21835E28D9516B00A622B3 /* CloudKey.swift in Sources */, + 6E21836528D9516B00A622B3 /* CloudLock.swift in Sources */, 6E21834228D8F3B500A622B3 /* UnlockEventManagedObject.swift in Sources */, 6E21833528D8F3B500A622B3 /* SetupEventManagedObject.swift in Sources */, 6E21834328D8F3B500A622B3 /* ManagedObject.swift in Sources */, @@ -581,14 +636,19 @@ 6E3276D228D70CE100AF171B /* Store.swift in Sources */, 6E4CB61A28D788AA00116573 /* UIStyleKit.swift in Sources */, 6E21834828D90CB800A622B3 /* EventsView.swift in Sources */, + 6E21836228D9516B00A622B3 /* CloudPermission.swift in Sources */, 6E21834628D8FC4300A622B3 /* Task.swift in Sources */, + 6E21836128D9516B00A622B3 /* CloudKit.swift in Sources */, + 6E21836428D9516B00A622B3 /* CloudUser.swift in Sources */, 6E21834E28D91FDC00A622B3 /* Event.swift in Sources */, 6E21830E28D7FF2400A622B3 /* AppGroup.swift in Sources */, + 6E21836728D9516B00A622B3 /* CloudNewKey.swift in Sources */, 6E21831528D80FF900A622B3 /* ApplicationData.swift in Sources */, 6E21833A28D8F3B500A622B3 /* LockManagedObject.swift in Sources */, 6E21830328D7C47500A622B3 /* Appearance.swift in Sources */, 6E3276DC28D7195400AF171B /* Central.swift in Sources */, 6E21833928D8F3B500A622B3 /* ScheduleManagedObject.swift in Sources */, + 6E21836028D9516B00A622B3 /* CloudEvent.swift in Sources */, 6E21831328D80FDD00A622B3 /* JSON.swift in Sources */, 6E21832128D8501500A622B3 /* ProgressIndicatorView.swift in Sources */, 6E21833828D8F3B500A622B3 /* NewKeyManagedObject.swift in Sources */, @@ -989,6 +1049,14 @@ kind = branch; }; }; + 6E21836828D953D000A622B3 /* XCRemoteSwiftPackageReference "CloudKitCodable" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/colemancda/CloudKitCodable.git"; + requirement = { + branch = master; + kind = branch; + }; + }; 6E3276D528D70FA000AF171B /* XCRemoteSwiftPackageReference "GATT" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/PureSwift/GATT.git"; @@ -1005,6 +1073,11 @@ package = 6E21830828D7FEA900A622B3 /* XCRemoteSwiftPackageReference "KeychainAccess" */; productName = KeychainAccess; }; + 6E21836928D953D000A622B3 /* CloudKitCodable */ = { + isa = XCSwiftPackageProductDependency; + package = 6E21836828D953D000A622B3 /* XCRemoteSwiftPackageReference "CloudKitCodable" */; + productName = CloudKitCodable; + }; 6E3276BB28D708A000AF171B /* CoreLock */ = { isa = XCSwiftPackageProductDependency; productName = CoreLock; diff --git a/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 917ecfb5..c8488a70 100644 --- a/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -18,6 +18,15 @@ "revision" : "ddd5493bd5382ca0132b8ca121dd35c7554ab7d0" } }, + { + "identity" : "cloudkitcodable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/colemancda/CloudKitCodable.git", + "state" : { + "branch" : "master", + "revision" : "598e04c5c6f1162d51fdc45d11c85574d39ee85c" + } + }, { "identity" : "gatt", "kind" : "remoteSourceControl", diff --git a/Xcode/SmartLock/Info.plist b/Xcode/SmartLock/Info.plist index 4d33c6e6..aff95fb9 100644 --- a/Xcode/SmartLock/Info.plist +++ b/Xcode/SmartLock/Info.plist @@ -6,6 +6,8 @@ bluetooth-central fetch + location + processing remote-notification diff --git a/Xcode/SmartLock/SmartLock.entitlements b/Xcode/SmartLock/SmartLock.entitlements index d32495af..e9434ab9 100644 --- a/Xcode/SmartLock/SmartLock.entitlements +++ b/Xcode/SmartLock/SmartLock.entitlements @@ -2,6 +2,20 @@ + aps-environment + development + com.apple.developer.aps-environment + development + com.apple.developer.icloud-container-identifiers + + iCloud.com.colemancda.Lock + + com.apple.developer.icloud-services + + CloudKit + + com.apple.developer.ubiquity-kvstore-identifier + $(TeamIdentifierPrefix)$(CFBundleIdentifier) com.apple.security.app-sandbox com.apple.security.application-groups @@ -14,6 +28,8 @@ com.apple.security.files.user-selected.read-only + com.apple.security.personal-information.addressbook + com.apple.security.personal-information.location keychain-access-groups From ec5b12d82da652d4ca8a0971dc306bc48c734ad3 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 23:02:12 -0700 Subject: [PATCH 085/229] [App] Prevent excessing file writing --- Xcode/LockKit/Model/FileManager.swift | 2 ++ Xcode/LockKit/Model/Store.swift | 12 +++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Xcode/LockKit/Model/FileManager.swift b/Xcode/LockKit/Model/FileManager.swift index b389c757..5b3c8b6c 100644 --- a/Xcode/LockKit/Model/FileManager.swift +++ b/Xcode/LockKit/Model/FileManager.swift @@ -155,6 +155,8 @@ private extension FileManager.Lock { assertionFailure("Could not encode \(T.self) to \(file.rawValue)") #endif } + + log("🗄️ Wrote file \(file.rawValue).json") } } diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index ed4d575d..52db18de 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -190,11 +190,17 @@ public extension Store { } } set { - objectWillChange.send() let oldValue = fileManager.applicationData + guard oldValue != newValue else { + return + } + // write file fileManager.applicationData = newValue - if oldValue?.locks != newValue.locks { - lockCacheChanged() + // emit combine change + objectWillChange.send() + // update CoreData and CloudKit + Task { + await lockCacheChanged() } } } From 677d1af5387bafed1c422360dbd65ba61250f210 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 19 Sep 2022 23:48:28 -0700 Subject: [PATCH 086/229] [App] Fixed iCloud queries --- .../Model/CoreData/ContactManagedObject.swift | 19 +++-- Xcode/LockKit/Model/Store.swift | 21 +++-- Xcode/LockKit/Model/iCloud/CloudKit.swift | 80 +++++++++++-------- Xcode/LockKit/Model/iCloud/iCloud.swift | 3 + Xcode/SmartLock/App.swift | 4 + 5 files changed, 74 insertions(+), 53 deletions(-) diff --git a/Xcode/LockKit/Model/CoreData/ContactManagedObject.swift b/Xcode/LockKit/Model/CoreData/ContactManagedObject.swift index cfb1449e..f5c75064 100644 --- a/Xcode/LockKit/Model/CoreData/ContactManagedObject.swift +++ b/Xcode/LockKit/Model/CoreData/ContactManagedObject.swift @@ -73,22 +73,23 @@ public extension ContactManagedObject { // MARK: - Store public extension Store { - /* - #if os(iOS) - func updateContacts() throws { + + func updateContacts() async throws { // exclude self - let currentUser = try cloud.container.fetchUserRecordID() + let currentUser = try await cloud.container.fetchUserRecordID() // insert new contacts var insertedUsers = Set() - let context = backgroundContext - try cloud.container.discoverAllUserIdentities { (user) in + for try await user in cloud.container.discoverAllUserIdentities() { guard let userRecordID = user.userRecordID, userRecordID != currentUser else { return } insertedUsers.insert(userRecordID.recordName) - context.commit { try $0.insert(contact: user) } + // save in CoreData + await backgroundContext.commit { + try $0.insert(contact: user) + } } // delete old contacts @@ -98,14 +99,12 @@ public extension Store { .init(keyPath: \ContactManagedObject.identifier, ascending: true) ] fetchRequest.predicate = NSPredicate(format: "NONE %K IN %@", #keyPath(ContactManagedObject.identifier), insertedUsers) - context.commit { (context) in + await backgroundContext.commit { (context) in try context.fetch(fetchRequest).forEach { context.delete($0) } } } - #endif - */ } internal extension ContactManagedObject { diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 52db18de..9eb471cc 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -176,7 +176,11 @@ public extension Store { public extension Store { private func lockCacheChanged() async { + // update CoreData await updateCoreData() + // update CloudKit + //do { try await uploadCloudApplicationData() } + //catch { log("⚠️ Unable to upload locks to iCloud") } } var applicationData: ApplicationData { @@ -603,12 +607,16 @@ public extension Store { notification updateBlock: @escaping ((EventsList, Bool) -> ()) = { _,_ in } ) async throws -> Bool { + var events = [LockEvent]() + // get lock key guard let information = self.lockInformation[lock], let lockCache = self[lock: information.id], let keyData = self[key: lockCache.key.id] else { return false } + let lockIdentifier = information.id + let key = KeyCredentials( id: lockCache.key.id, secret: keyData @@ -620,11 +628,11 @@ public extension Store { let centralLog = central.log try await central.connection(for: lock) { let stream = try await $0.listEvents(fetchRequest: fetchRequest, using: key, log: centralLog) - var events = [LockEvent]() for try await notification in stream { if let event = notification.event { events.append(event) centralLog?("Recieved event \(event.id)") + // store in CoreData await context.commit { (context) in try context.insert(event, for: information.id) } @@ -635,23 +643,20 @@ public extension Store { } } - /* - #if os(iOS) + // upload to iCloud if preferences.isCloudBackupEnabled { - DispatchQueue.cloud.async { [weak self] in - // upload to iCloud + // perform concurrently + Task { do { for event in events { let value = LockEvent.Cloud(event: event, for: lockIdentifier) - try self?.cloud.upload(value) + try await self.cloud.upload(value) } } catch { log("⚠️ Could not upload latest events to iCloud: \(error.localizedDescription)") } } } - #endif - */ log("Listed events for lock \(information.id)") diff --git a/Xcode/LockKit/Model/iCloud/CloudKit.swift b/Xcode/LockKit/Model/iCloud/CloudKit.swift index 507d90c9..a923b24a 100644 --- a/Xcode/LockKit/Model/iCloud/CloudKit.swift +++ b/Xcode/LockKit/Model/iCloud/CloudKit.swift @@ -232,9 +232,7 @@ internal extension CKDatabase { operation.configuration.isLongLived = false operation.configuration.allowsCellularAccess = true operation.configuration.qualityOfService = .userInitiated - fatalError() - /* - return AsyncThrowingStream(CKQueryOperation.AsyncStreamValue.self, bufferingPolicy: .unbounded) { (continuation: AsyncThrowingStream.Continuation) in + return .init(CKQueryOperation.AsyncStreamValue.self, bufferingPolicy: .unbounded) { continuation in operation.recordMatchedBlock = { (id, result) in switch result { @@ -244,62 +242,74 @@ internal extension CKDatabase { continuation.finish(throwing: error) } } - operation.queryResultBlock { (result) in + + operation.queryResultBlock = { (result) in switch result { case let .success(value): if let cursor = value { - continuation.yield(.cursor(value)) + continuation.yield(.cursor(cursor)) } continuation.finish() case let .failure(error): continuation.finish(throwing: error) } } + add(operation) - }*/ + } } func queryAll( _ query: CKQuery, zone: CKRecordZone.ID? = nil - ) -> AsyncThrowingStream { + ) -> AsyncThrowingStream { let operation = CKQueryOperation(query: query) operation.zoneID = zone operation.configuration.isLongLived = false operation.configuration.allowsCellularAccess = true operation.configuration.qualityOfService = .userInitiated - fatalError() - /* - return AsyncThrowingStream.init(CKRecord.self, bufferingPolicy: .unbounded) { continuation in - let task = Task.detached { - var cursor: CKQueryOperation.Cursor? - // first request - for try await value in self.query(operation) { - switch value { - case let .record(record): - continuation.yield(record) - case let .cursor(newCursor): - cursor = newCursor - } - } - // continue fetching if cursor returned - while let queryCursor = cursor { - let cursorOperation = CKQueryOperation(cursor: queryCursor) - cursorOperation.zoneID = zone - for try await value in self.query(operation) { - switch value { - case let .record(record): - continuation.yield(record) - case let .cursor(newCursor): - cursor = newCursor + return AsyncThrowingStream(CKRecord.self, bufferingPolicy: .unbounded) { (continuation: AsyncThrowingStream.Continuation) in + do { + let task = Task.detached { + do { + var cursor: CKQueryOperation.Cursor? + // first request + for try await value in self.query(operation) { + switch value { + case let .record(record): + continuation.yield(record) + case let .cursor(newCursor): + cursor = newCursor + } } + + // continue fetching if cursor returned + while let queryCursor = cursor { + let cursorOperation = CKQueryOperation(cursor: queryCursor) + cursorOperation.zoneID = zone + for try await value in self.query(operation) { + switch value { + case let .record(record): + continuation.yield(record) + case let .cursor(newCursor): + cursor = newCursor + } + } + } + + // end stream + continuation.finish() + } catch { + continuation.finish(throwing: error) } } + /* + continuation.onTermination = { + task.cancel() + }*/ } - continuation.onTermination = { - task.cancel() - } - }*/ + return + } } func modifyZones( diff --git a/Xcode/LockKit/Model/iCloud/iCloud.swift b/Xcode/LockKit/Model/iCloud/iCloud.swift index 39aabb5b..f3c35201 100644 --- a/Xcode/LockKit/Model/iCloud/iCloud.swift +++ b/Xcode/LockKit/Model/iCloud/iCloud.swift @@ -324,6 +324,8 @@ public extension Store { try await cloud.accountStatus() == .available else { return } + log("☁️ Will sync with iCloud") + // download to CoreData try await downloadCloudLocks() @@ -335,6 +337,7 @@ public extension Store { // download and upload application data if try await downloadCloudApplicationData(conflicts: conflicts) { + // upload merged app data try await uploadCloudApplicationData() } diff --git a/Xcode/SmartLock/App.swift b/Xcode/SmartLock/App.swift index 3219b44d..87463fe7 100644 --- a/Xcode/SmartLock/App.swift +++ b/Xcode/SmartLock/App.swift @@ -18,6 +18,10 @@ struct LockApp: App { .environment(\.managedObjectContext, Store.shared.managedObjectContext) .onAppear { _ = LockApp.initialize + Task { + do { try await Store.shared.syncCloud() } + catch { log("⚠️ Unable to automatically sync with iCloud") } + } } .onContinueUserActivity("") { _ in From b65a45286550f5005a5a379164ca1f9d2164975a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 00:24:23 -0700 Subject: [PATCH 087/229] [App] Fixed iCloud sync --- Xcode/LockKit/Model/iCloud/iCloud.swift | 9 +++++---- Xcode/LockKit/View/LockDetailView.swift | 4 +++- Xcode/SmartLock/App.swift | 4 ---- Xcode/SmartLock/View/SidebarView.swift | 7 +++++++ Xcode/SmartLock/View/TabBarView.swift | 6 ++++++ 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/Xcode/LockKit/Model/iCloud/iCloud.swift b/Xcode/LockKit/Model/iCloud/iCloud.swift index f3c35201..8a471e00 100644 --- a/Xcode/LockKit/Model/iCloud/iCloud.swift +++ b/Xcode/LockKit/Model/iCloud/iCloud.swift @@ -461,10 +461,6 @@ public extension Store { // Import application data let oldApplicationData = self.applicationData - guard cloudData != oldApplicationData else { - log("☁️ No new data from iCloud") - return false - } #if DEBUG let dateFormatter = DateFormatter() @@ -476,6 +472,11 @@ public extension Store { dump(oldApplicationData) #endif + guard cloudData != oldApplicationData else { + log("☁️ No new data from iCloud") + return false + } + // attempt to overwrite if let newData = oldApplicationData.update(with: cloudData) { // write new application data diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index c80d2d26..29088362 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -64,7 +64,7 @@ private extension LockDetailView { try await store.unlock(for: id, action: .default) } catch { - log("⚠️ Unable to unlock \(id)") + log("⚠️ Unable to unlock \(id). \(error)") } } @@ -130,6 +130,7 @@ extension LockDetailView { PermissionIconView(permission: cache.key.permission.type) .frame(width: 150, height: 150, alignment: .center) }) + .buttonStyle(.plain) //.disabled(enableActions == false) .padding(30) Spacer() @@ -213,6 +214,7 @@ extension LockDetailView { } } .padding(20) + .buttonStyle(.plain) } .navigationTitle(Text(verbatim: cache.name)) } diff --git a/Xcode/SmartLock/App.swift b/Xcode/SmartLock/App.swift index 87463fe7..3219b44d 100644 --- a/Xcode/SmartLock/App.swift +++ b/Xcode/SmartLock/App.swift @@ -18,10 +18,6 @@ struct LockApp: App { .environment(\.managedObjectContext, Store.shared.managedObjectContext) .onAppear { _ = LockApp.initialize - Task { - do { try await Store.shared.syncCloud() } - catch { log("⚠️ Unable to automatically sync with iCloud") } - } } .onContinueUserActivity("") { _ in diff --git a/Xcode/SmartLock/View/SidebarView.swift b/Xcode/SmartLock/View/SidebarView.swift index a33d5ba2..9304db09 100644 --- a/Xcode/SmartLock/View/SidebarView.swift +++ b/Xcode/SmartLock/View/SidebarView.swift @@ -38,6 +38,13 @@ struct SidebarView: View { ) detail } + .navigationViewStyle(.columns) + .onAppear { + Task { + do { try await Store.shared.syncCloud(conflicts: { _ in return true }) } // always override on macOS + catch { log("⚠️ Unable to automatically sync with iCloud") } + } + } } } diff --git a/Xcode/SmartLock/View/TabBarView.swift b/Xcode/SmartLock/View/TabBarView.swift index b6b00bfe..1c549339 100644 --- a/Xcode/SmartLock/View/TabBarView.swift +++ b/Xcode/SmartLock/View/TabBarView.swift @@ -49,6 +49,12 @@ struct TabBarView: View { } .navigationViewStyle(.stack) .navigationBarTitleDisplayMode(.large) + .onAppear { + Task { + do { try await Store.shared.syncCloud() } + catch { log("⚠️ Unable to automatically sync with iCloud") } + } + } } } From d771a39fea1e24ecaa47d125789aa0ad25e3297a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 00:55:35 -0700 Subject: [PATCH 088/229] [App] Added `NavigationLink` for macOS --- Xcode/LockKit/View/LockDetailView.swift | 4 +- Xcode/LockKit/View/NavigationLink.swift | 43 ++++++++++++++++++++ Xcode/SmartLock.xcodeproj/project.pbxproj | 4 ++ Xcode/SmartLock/View/KeysView.swift | 2 +- Xcode/SmartLock/View/NearbyDevicesView.swift | 2 +- Xcode/SmartLock/View/SidebarView.swift | 23 +++++++++-- 6 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 Xcode/LockKit/View/NavigationLink.swift diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 29088362..277c471e 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -184,7 +184,7 @@ extension LockDetailView { .frame(width: titleWidth, height: nil, alignment: .leading) .font(.body) .foregroundColor(.gray) - NavigationLink(destination: { + AppNavigationLink(destination: { EventsView(lock: id) }, label: { HStack { @@ -200,7 +200,7 @@ extension LockDetailView { .frame(width: titleWidth, height: nil, alignment: .leading) .font(.body) .foregroundColor(.gray) - NavigationLink(destination: { + AppNavigationLink(destination: { Text("Keys") }, label: { HStack { diff --git a/Xcode/LockKit/View/NavigationLink.swift b/Xcode/LockKit/View/NavigationLink.swift new file mode 100644 index 00000000..b33dfc1b --- /dev/null +++ b/Xcode/LockKit/View/NavigationLink.swift @@ -0,0 +1,43 @@ +// +// NavigationLink.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/20/22. +// + +import SwiftUI + +#if os(macOS) + +public var AppNavigationLinkNavigate: (AnyView) -> () = { _ in assertionFailure() } + +public struct AppNavigationLink : View { + + private let destination: Destination + + private let label: Label + + public var body: some View { + Button( + action: buttonAction, + label: { label } + ) + .buttonStyle(.plain) + } + + public init(destination: () -> Destination, label: () -> Label) { + self.destination = destination() + self.label = label() + } +} + +private extension AppNavigationLink { + + func buttonAction() { + AppNavigationLinkNavigate(AnyView(destination)) + } +} + +#else +public typealias AppNavigationLink = SwiftUI.NavigationLink +#endif diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index e888a9e9..840aef29 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 6E169A7828D9A34F008545EC /* NavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7728D9A34F008545EC /* NavigationLink.swift */; }; 6E2182EF28D7B37000A622B3 /* TabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2182EE28D7B37000A622B3 /* TabBarView.swift */; }; 6E2182F128D7B4FA00A622B3 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2182F028D7B4FA00A622B3 /* SettingsView.swift */; }; 6E21830128D7C37500A622B3 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21830028D7C37500A622B3 /* Error.swift */; }; @@ -108,6 +109,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 6E169A7728D9A34F008545EC /* NavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationLink.swift; sourceTree = ""; }; 6E2182EE28D7B37000A622B3 /* TabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarView.swift; sourceTree = ""; }; 6E2182F028D7B4FA00A622B3 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 6E21830028D7C37500A622B3 /* Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; @@ -335,6 +337,7 @@ 6E4CB61928D788AA00116573 /* PermissionIconView.swift */, 6E21832228D8D26300A622B3 /* LockDetailView.swift */, 6E21834728D90CB800A622B3 /* EventsView.swift */, + 6E169A7728D9A34F008545EC /* NavigationLink.swift */, ); path = View; sourceTree = ""; @@ -622,6 +625,7 @@ 6E21830728D7D08300A622B3 /* Permission.swift in Sources */, 6E21833F28D8F3B500A622B3 /* LockInformationManagedObject.swift in Sources */, 6E21833D28D8F3B500A622B3 /* EventManagedObject.swift in Sources */, + 6E169A7828D9A34F008545EC /* NavigationLink.swift in Sources */, 6E21836628D9516B00A622B3 /* CloudNewKeyInvitation.swift in Sources */, 6E21834128D8F3B500A622B3 /* Model.xcdatamodeld in Sources */, 6E21833728D8F3B500A622B3 /* PersistentContainer.swift in Sources */, diff --git a/Xcode/SmartLock/View/KeysView.swift b/Xcode/SmartLock/View/KeysView.swift index 8b28cc38..50565544 100644 --- a/Xcode/SmartLock/View/KeysView.swift +++ b/Xcode/SmartLock/View/KeysView.swift @@ -45,7 +45,7 @@ private extension KeysView.StateView { var list: some View { List(items) { (item) in - NavigationLink(destination: { + AppNavigationLink(destination: { LockDetailView(id: item.id) }, label: { LockRowView(item) diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index c91b85ff..2a6c7439 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -131,7 +131,7 @@ private extension NearbyDevicesView.StateView { case .loading, .unknown: LockRowView(item) case .key, .setup: - NavigationLink(destination: { + AppNavigationLink(destination: { destination(item) }, label: { LockRowView(item) diff --git a/Xcode/SmartLock/View/SidebarView.swift b/Xcode/SmartLock/View/SidebarView.swift index 9304db09..df192b55 100644 --- a/Xcode/SmartLock/View/SidebarView.swift +++ b/Xcode/SmartLock/View/SidebarView.swift @@ -15,7 +15,7 @@ struct SidebarView: View { var store: Store @State - var selection: Item.ID? + private var selection: Item.ID? @State private var isNearbyExpanded = true @@ -23,10 +23,16 @@ struct SidebarView: View { @State private var isKeysExpanded = true + @State + private var detail: AnyView? + var body: some View { SwiftUI.NavigationView { SidebarView.NavigationView( - selection: $selection, + selection: Binding( + get: { selection }, + set: { selectionChanged($0) } + ), isScanning: store.isScanning, locks: locks, keys: keys, @@ -36,10 +42,14 @@ struct SidebarView: View { ), isKeysExpanded: $isKeysExpanded ) - detail + detail ?? AnyView(Text("Select a lock")) } .navigationViewStyle(.columns) .onAppear { + // configure navigation links + AppNavigationLinkNavigate = { + self.detail = $0 + } Task { do { try await Store.shared.syncCloud(conflicts: { _ in return true }) } // always override on macOS catch { log("⚠️ Unable to automatically sync with iCloud") } @@ -80,6 +90,11 @@ private extension SidebarView { } } + func selectionChanged(_ newValue: Item.ID?) { + selection = newValue + detail = selectionDetail + } + func item(for peripheral: NativePeripheral) -> Item { if let information = store.lockInformation[peripheral] { switch information.status { @@ -101,7 +116,7 @@ private extension SidebarView { } } - var detail: some View { + var selectionDetail: AnyView { guard let selection = self.selection, let item = locks.first(where: { $0.id == selection }) ?? keys.first(where: { $0.id == selection }) else { return AnyView( Text("Select a lock") From eccdd6e3fa561e22b53c129d62d4ed79ab2ef8a7 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 01:05:01 -0700 Subject: [PATCH 089/229] [App] Fix sidebar selection --- Xcode/SmartLock.xcodeproj/project.pbxproj | 2 ++ Xcode/SmartLock/View/SidebarView.swift | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 840aef29..75d5597b 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 6E169A7828D9A34F008545EC /* NavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7728D9A34F008545EC /* NavigationLink.swift */; }; + 6E169A7928D9AA32008545EC /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21834528D8FC4300A622B3 /* Task.swift */; }; 6E2182EF28D7B37000A622B3 /* TabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2182EE28D7B37000A622B3 /* TabBarView.swift */; }; 6E2182F128D7B4FA00A622B3 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2182F028D7B4FA00A622B3 /* SettingsView.swift */; }; 6E21830128D7C37500A622B3 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21830028D7C37500A622B3 /* Error.swift */; }; @@ -590,6 +591,7 @@ files = ( 6E2182F128D7B4FA00A622B3 /* SettingsView.swift in Sources */, 6E4CB62528D792BF00116573 /* InfoPlist.swift in Sources */, + 6E169A7928D9AA32008545EC /* Task.swift in Sources */, 6E21831D28D834D200A622B3 /* ContentView.swift in Sources */, 6EA7768528D7061600018FA3 /* App.swift in Sources */, 6E2182EF28D7B37000A622B3 /* TabBarView.swift in Sources */, diff --git a/Xcode/SmartLock/View/SidebarView.swift b/Xcode/SmartLock/View/SidebarView.swift index df192b55..cfe8c3c4 100644 --- a/Xcode/SmartLock/View/SidebarView.swift +++ b/Xcode/SmartLock/View/SidebarView.swift @@ -92,7 +92,14 @@ private extension SidebarView { func selectionChanged(_ newValue: Item.ID?) { selection = newValue + guard let _ = newValue else { + return // don't allow deselecting + } detail = selectionDetail + Task { + try await Task.sleep(timeInterval: 0.2) + selection = nil + } } func item(for peripheral: NativePeripheral) -> Item { From 3d3751b6ba4b7fd802f8181533e78d705a6bf68e Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 01:44:13 -0700 Subject: [PATCH 090/229] [App] Fixed loading events --- Xcode/LockKit/Model/Store.swift | 4 ++- Xcode/LockKit/View/EventsView.swift | 46 +++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 9eb471cc..73372d13 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -593,7 +593,9 @@ public extension Store { } // upload keys to cloud - //updateCloud() + Task { + //updateCloud() + } log("Listed keys for lock \(information.id)") diff --git a/Xcode/LockKit/View/EventsView.swift b/Xcode/LockKit/View/EventsView.swift index 742bdd04..597d8b5f 100644 --- a/Xcode/LockKit/View/EventsView.swift +++ b/Xcode/LockKit/View/EventsView.swift @@ -23,7 +23,8 @@ public struct EventsView: View { sortDescriptors: [ NSSortDescriptor(keyPath: \EventManagedObject.date, ascending: false) ], - predicate: nil + predicate: nil, + animation: .linear ) var events: FetchedResults @@ -77,6 +78,7 @@ private extension EventsView { List(events) { row(for: $0) } + .listStyle(.plain) .refreshable { reload() } @@ -86,7 +88,47 @@ private extension EventsView { } func reload() { - + let locks = locks.sorted(by: { $0.description < $1.description }) + Task { + let context = store.backgroundContext + store.stopScanning() + for lock in locks { + // load via Bonjour + /* + do { + + } catch { + log("⚠️ Error loading events for \(lock) via Bonjour") + }*/ + // scan and find device + do { + if let peripheral = try await store.device(for: lock) { + // load keys if admin + if let permssion = store[lock: lock]?.key.permission, permssion.isAdministrator { + let _ = try await store.listKeys(peripheral) + } + // load latest events + var lastEventDate: Date? + try? await context.perform { + lastEventDate = try context.find(id: lock, type: LockManagedObject.self) + .flatMap { try $0.lastEvent(in: context)?.date } + } + let fetchRequest = LockEvent.FetchRequest( + offset: 0, + limit: nil, + predicate: LockEvent.Predicate( + keys: nil, + start: lastEventDate, + end: nil + ) + ) + let _ = try await store.listEvents(peripheral, fetchRequest: fetchRequest) + } + } catch { + log("⚠️ Error loading events for \(lock)") + } + } + } } func row(for managedObject: EventManagedObject) -> some View { From ad066a251275b6848cbba5c934d82a09f135d0d1 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 02:04:59 -0700 Subject: [PATCH 091/229] [App] Fixed refreshing lock data --- Xcode/LockKit/Model/Store.swift | 2 + Xcode/LockKit/View/LockDetailView.swift | 50 +++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 73372d13..552a1ab2 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -591,6 +591,7 @@ public extension Store { } } } + objectWillChange.send() // upload keys to cloud Task { @@ -644,6 +645,7 @@ public extension Store { } } + objectWillChange.send() // upload to iCloud if preferences.isCloudBackupEnabled { diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 277c471e..56bb30ce 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -29,6 +29,12 @@ public struct LockDetailView: View { keys: keys, unlock: unlock ) + .refreshable { + reload() + } + .onAppear { + reload() + } ) } else if let information = self.information, information.status == .setup { @@ -81,6 +87,50 @@ private extension LockDetailView { var keys: Int { 1 } + + func reload() { + let lock = self.id + Task { + let context = store.backgroundContext + store.stopScanning() + // load via Bonjour + /* + do { + + } catch { + log("⚠️ Error loading events for \(lock) via Bonjour") + }*/ + // scan and find device + do { + if let peripheral = try await store.device(for: lock) { + // read information + let _ = try await store.readInformation(for: peripheral) + // load keys if admin + if let permssion = store[lock: lock]?.key.permission, permssion.isAdministrator { + let _ = try await store.listKeys(peripheral) + } + // load latest events + var lastEventDate: Date? + try? await context.perform { + lastEventDate = try context.find(id: lock, type: LockManagedObject.self) + .flatMap { try $0.lastEvent(in: context)?.date } + } + let fetchRequest = LockEvent.FetchRequest( + offset: 0, + limit: nil, + predicate: LockEvent.Predicate( + keys: nil, + start: lastEventDate, + end: nil + ) + ) + let _ = try await store.listEvents(peripheral, fetchRequest: fetchRequest) + } + } catch { + log("⚠️ Error loading information for \(lock)") + } + } + } } extension LockDetailView { From b4e23a9bce3fd88e2af4e90656dc10194e814957 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 02:13:12 -0700 Subject: [PATCH 092/229] [App] Fixed keys count --- Xcode/LockKit/View/LockDetailView.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 56bb30ce..e9e4add1 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -85,7 +85,13 @@ private extension LockDetailView { } var keys: Int { - 1 + let fetchRequest = KeyManagedObject.fetchRequest() + fetchRequest.predicate = NSPredicate( + format: "%K == %@", + #keyPath(KeyManagedObject.lock.identifier), + id as NSUUID + ) + return (try? managedObjectContext.count(for: fetchRequest)) ?? 0 } func reload() { From 77884aac7d1c602fcf446ce02fe3659ed5744cf8 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 02:47:41 -0700 Subject: [PATCH 093/229] [App] Added `PermissionScheduleView` --- Xcode/LockKit/View/LockDetailView.swift | 7 - Xcode/LockKit/View/NewPermissionView.swift | 20 ++ .../LockKit/View/PermissionScheduleView.swift | 326 ++++++++++++++++++ Xcode/LockKit/View/PermissionsView.swift | 20 ++ Xcode/SmartLock.xcodeproj/project.pbxproj | 35 +- .../xcshareddata/swiftpm/Package.resolved | 9 + 6 files changed, 407 insertions(+), 10 deletions(-) create mode 100644 Xcode/LockKit/View/NewPermissionView.swift create mode 100644 Xcode/LockKit/View/PermissionScheduleView.swift create mode 100644 Xcode/LockKit/View/PermissionsView.swift diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index e9e4add1..36e269ed 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -99,13 +99,6 @@ private extension LockDetailView { Task { let context = store.backgroundContext store.stopScanning() - // load via Bonjour - /* - do { - - } catch { - log("⚠️ Error loading events for \(lock) via Bonjour") - }*/ // scan and find device do { if let peripheral = try await store.device(for: lock) { diff --git a/Xcode/LockKit/View/NewPermissionView.swift b/Xcode/LockKit/View/NewPermissionView.swift new file mode 100644 index 00000000..ba0b205f --- /dev/null +++ b/Xcode/LockKit/View/NewPermissionView.swift @@ -0,0 +1,20 @@ +// +// NewPermissionView.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/20/22. +// + +import SwiftUI + +struct NewPermissionView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +struct NewPermissionView_Previews: PreviewProvider { + static var previews: some View { + NewPermissionView() + } +} diff --git a/Xcode/LockKit/View/PermissionScheduleView.swift b/Xcode/LockKit/View/PermissionScheduleView.swift new file mode 100644 index 00000000..31aed4dc --- /dev/null +++ b/Xcode/LockKit/View/PermissionScheduleView.swift @@ -0,0 +1,326 @@ +// +// PermissionScheduleView.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 8/30/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import SwiftUI +import CoreLock +import SFSafeSymbols + +/// Permission Schedule View +public struct PermissionScheduleView: View { + + // MARK: - Properties + + public init(schedule: Permission.Schedule = .init()) { + _schedule = Binding(get: { schedule }, set: { _ in }) // read only + } + + public init(schedule: Binding) { + _schedule = schedule + } + + // MARK: - Properties + + @Binding + public var schedule: Permission.Schedule + + @State + private var defaultExpiration = Date() + (60 * 60 * 24) + + @State + private var defaultInterval: Permission.Schedule.Interval = .default + + private var expiration: Binding { + return Binding(get: { + return self.schedule.expiry ?? self.defaultExpiration + }, set: { + self.schedule.expiry = $0 + self.defaultExpiration = $0 + }) + } + + private var doesExpire: Bool { + return self.schedule.expiry != nil + } + + private var isCustomSchedule: Bool { + return self.schedule.interval != .anytime + } + + private var showExpirationPicker: Binding { + return Binding(get: { + return self.schedule.expiry != nil + }, set: { (showPicker) in + if showPicker { + self.schedule.expiry = self.schedule.expiry ?? self.defaultExpiration + } else { + self.schedule.expiry = nil + } + }) + } + + private var checkmark: some View { + return Image(systemSymbol: .checkmark) + .foregroundColor(Color.orange) + } + + private static let expirationTimeFormatter = RelativeDateTimeFormatter() + + private func expirationTime(for date: Date) -> Text { + let expiration: String + let timeRemaining = date.timeIntervalSinceNow + if timeRemaining > 0 { + let timeFormatter = type(of: self).expirationTimeFormatter + expiration = timeFormatter.localizedString(fromTimeInterval: timeRemaining) + } else { + expiration = "Expired" // TODO: Localize + } + return Text(verbatim: expiration) + } + + private static let intervalTimeFormatter: RelativeDateTimeFormatter = { + let formatter = RelativeDateTimeFormatter() + return formatter + }() + + private var intervalStart: Binding { + return Binding(get: { + let minutes = self.schedule.interval.rawValue.lowerBound + return self.date(from: minutes) + }, set: { + let minutes = self.minutes(from: $0) + guard let interval = Permission.Schedule.Interval(rawValue: minutes ... self.schedule.interval.rawValue.upperBound) else { + assertionFailure() + return + } + self.schedule.interval = interval + self.defaultInterval = interval + }) + } + + private var intervalEnd: Binding { + return Binding(get: { + let minutes = self.schedule.interval.rawValue.upperBound + return self.date(from: minutes) + }, set: { + let minutes = self.minutes(from: $0) + guard let interval = Permission.Schedule.Interval(rawValue: self.schedule.interval.rawValue.lowerBound ... minutes) else { + assertionFailure() + return + } + self.schedule.interval = interval + self.defaultInterval = interval + }) + } + + private func minutes(from date: Date) -> UInt16 { + return UInt16(date.timeIntervalSinceReferenceDate / 60) + } + + private func date(from minutes: UInt16) -> Date { + return Date(timeIntervalSinceReferenceDate: TimeInterval(minutes) * 60) + } + + private func toggle(_ weekday: Permission.Schedule.Weekdays.Day) { + + var weekdays = schedule.weekdays + weekdays[weekday].toggle() + guard weekdays != .none else { return } + self.schedule.weekdays = weekdays // set new value + } + + // MARK: - View + + public var body: some View { + + Form { + + Section(header: Text(verbatim: "Time")) { + Button(action: { self.schedule.interval = .anytime }) { + HStack { + if self.schedule.interval == .anytime { + self.checkmark + } + Text(verbatim: "Any time") + .foregroundColor(Color.primary) + } + } + Button(action: { self.schedule.interval = self.defaultInterval }) { + HStack { + if isCustomSchedule { + self.checkmark + } + Text(verbatim: "Scheduled") + .foregroundColor(Color.primary) + } + } + if isCustomSchedule { + DatePicker( + selection: intervalStart, + in: date(from: 0) ... (intervalEnd.wrappedValue - 1), + displayedComponents: [.hourAndMinute], + label: { Text("Start") } + ) + DatePicker( + selection: intervalEnd, + in: (intervalStart.wrappedValue + 1) ... date(from: 60 * 24), + displayedComponents: [.hourAndMinute], + label: { Text("End") } + ) + } + } + + Section(header: Text(verbatim: "Expires")) { + Toggle(isOn: showExpirationPicker) { + schedule.expiry.flatMap({ expirationTime(for: $0) }) ?? Text("Never") + } + if showExpirationPicker.wrappedValue { + DatePicker(selection: expiration) { Text(verbatim: " ") } + } + } + + Section(header: Text(verbatim: "Days"), footer: SectionBottom(weekdays: schedule.weekdays)) { + HStack { + Spacer() + Text(verbatim: "S") + .modifier(RoundText(enabled: schedule.weekdays.sunday)) + .onTapGesture { self.toggle(.sunday) } + Text(verbatim: "M") + .modifier(RoundText(enabled: schedule.weekdays.monday)) + .onTapGesture { self.toggle(.monday) } + Text(verbatim: "T") + .modifier(RoundText(enabled: schedule.weekdays.tuesday)) + .onTapGesture { self.toggle(.tuesday) } + Text(verbatim: "W") + .modifier(RoundText(enabled: schedule.weekdays.wednesday)) + .onTapGesture { self.toggle(.wednesday) } + Text(verbatim: "T") + .modifier(RoundText(enabled: schedule.weekdays.thursday)) + .onTapGesture { self.toggle(.thursday) } + Text(verbatim: "F") + .modifier(RoundText(enabled: schedule.weekdays.friday)) + .onTapGesture { self.toggle(.friday) } + Text(verbatim: "S") + .modifier(RoundText(enabled: schedule.weekdays.saturday)) + .onTapGesture { self.toggle(.saturday) } + Spacer() + } + } + } + .listStyle(GroupedListStyle()) + .navigationBarTitle(Text("Schedule")) + } +} + +public extension PermissionScheduleView { + + struct Modal: View { + + public init(done: @escaping ((Permission.Schedule) -> ()), + cancel: @escaping (() -> ())) { + self.cancel = cancel + self.done = done + } + + public var done: ((Permission.Schedule) -> ()) + + public var cancel: (() -> ()) + + @State + public var schedule = Permission.Schedule() + + public var body: some View { + + NavigationView { + PermissionScheduleView(schedule: $schedule) + .navigationBarItems( + leading: Button( + action: { self.cancel() }, + label: { Text("Cancel") } + ), + trailing: Button( + action: { self.done(self.schedule) }, + label: { Text("Done") } + ) + ) + } + } + } +} + +struct RoundText: ViewModifier { + + // MARK: - Properties + + let enabled: Bool + + // MARK: - View Modifier + func body(content: Content) -> some View { + content + .frame(width: 15, height: 15) + .padding(10) + .foregroundColor(Color.white) + .background(color) + .mask(Circle()) + } +} + +extension RoundText { + + // MARK: - Properties + var color: SwiftUI.Color { + return enabled ? .orange : .gray + } +} + +public struct SectionBottom: View { + + // MARK: - Properties + let weekdays: Permission.Schedule.Weekdays + + // MARK: - View + + public var body: some View { + Text(verbatim: weekdays.localizedText) + } +} + +extension PermissionScheduleView { + + #if os(iOS) + typealias ListStyle = GroupedListStyle + #elseif os(watchOS) + typealias ListStyle = CarouselListStyle + #endif +} + +#if DEBUG +struct DayViewPreview: PreviewProvider { + static var previews: some View { + Group { + + NavigationView { + PermissionScheduleView() + } + .previewDevice("iPhone SE") + + NavigationView { + PermissionScheduleView() + } + .previewDevice("iPhone SE") + .environment(\.colorScheme, .dark) + + NavigationView { + PermissionScheduleView() + } + .previewDevice("iPhone XR") + .environment(\.colorScheme, .dark) + } + } +} +#endif diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift new file mode 100644 index 00000000..bd7fe197 --- /dev/null +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -0,0 +1,20 @@ +// +// PermissionsView.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/20/22. +// + +import SwiftUI + +struct PermissionsView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +struct PermissionsView_Previews: PreviewProvider { + static var previews: some View { + PermissionsView() + } +} diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 75d5597b..c4424fae 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -9,6 +9,10 @@ /* Begin PBXBuildFile section */ 6E169A7828D9A34F008545EC /* NavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7728D9A34F008545EC /* NavigationLink.swift */; }; 6E169A7928D9AA32008545EC /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21834528D8FC4300A622B3 /* Task.swift */; }; + 6E169A7D28D9C097008545EC /* PermissionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7C28D9C097008545EC /* PermissionsView.swift */; }; + 6E169A7F28D9C135008545EC /* NewPermissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7E28D9C135008545EC /* NewPermissionView.swift */; }; + 6E169A8128D9C15B008545EC /* PermissionScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A8028D9C15B008545EC /* PermissionScheduleView.swift */; }; + 6E169A8428D9C25C008545EC /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 6E169A8328D9C25C008545EC /* SFSafeSymbols */; }; 6E2182EF28D7B37000A622B3 /* TabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2182EE28D7B37000A622B3 /* TabBarView.swift */; }; 6E2182F128D7B4FA00A622B3 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2182F028D7B4FA00A622B3 /* SettingsView.swift */; }; 6E21830128D7C37500A622B3 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21830028D7C37500A622B3 /* Error.swift */; }; @@ -111,6 +115,9 @@ /* Begin PBXFileReference section */ 6E169A7728D9A34F008545EC /* NavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationLink.swift; sourceTree = ""; }; + 6E169A7C28D9C097008545EC /* PermissionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsView.swift; sourceTree = ""; }; + 6E169A7E28D9C135008545EC /* NewPermissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPermissionView.swift; sourceTree = ""; }; + 6E169A8028D9C15B008545EC /* PermissionScheduleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PermissionScheduleView.swift; sourceTree = ""; }; 6E2182EE28D7B37000A622B3 /* TabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarView.swift; sourceTree = ""; }; 6E2182F028D7B4FA00A622B3 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 6E21830028D7C37500A622B3 /* Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; @@ -208,6 +215,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6E169A8428D9C25C008545EC /* SFSafeSymbols in Frameworks */, 6E21830A28D7FEA900A622B3 /* KeychainAccess in Frameworks */, 6E3276D928D70FA000AF171B /* GATT in Frameworks */, 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */, @@ -334,11 +342,14 @@ children = ( 6E4CB61D28D7901D00116573 /* AppKit */, 6E4CB61C28D788B900116573 /* UIKit */, - 6E3276E528D782B900AF171B /* LockRowView.swift */, - 6E4CB61928D788AA00116573 /* PermissionIconView.swift */, - 6E21832228D8D26300A622B3 /* LockDetailView.swift */, 6E21834728D90CB800A622B3 /* EventsView.swift */, + 6E21832228D8D26300A622B3 /* LockDetailView.swift */, + 6E3276E528D782B900AF171B /* LockRowView.swift */, 6E169A7728D9A34F008545EC /* NavigationLink.swift */, + 6E4CB61928D788AA00116573 /* PermissionIconView.swift */, + 6E169A7C28D9C097008545EC /* PermissionsView.swift */, + 6E169A7E28D9C135008545EC /* NewPermissionView.swift */, + 6E169A8028D9C15B008545EC /* PermissionScheduleView.swift */, ); path = View; sourceTree = ""; @@ -498,6 +509,7 @@ 6E3276D828D70FA000AF171B /* GATT */, 6E21830928D7FEA900A622B3 /* KeychainAccess */, 6E21836928D953D000A622B3 /* CloudKitCodable */, + 6E169A8328D9C25C008545EC /* SFSafeSymbols */, ); productName = LockKit; productReference = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; @@ -538,6 +550,7 @@ 6E3276D528D70FA000AF171B /* XCRemoteSwiftPackageReference "GATT" */, 6E21830828D7FEA900A622B3 /* XCRemoteSwiftPackageReference "KeychainAccess" */, 6E21836828D953D000A622B3 /* XCRemoteSwiftPackageReference "CloudKitCodable" */, + 6E169A8228D9C25C008545EC /* XCRemoteSwiftPackageReference "SFSafeSymbols" */, ); productRefGroup = 6EA7768228D7061600018FA3 /* Products */; projectDirPath = ""; @@ -607,6 +620,7 @@ buildActionMask = 2147483647; files = ( 6E21832328D8D26300A622B3 /* LockDetailView.swift in Sources */, + 6E169A8128D9C15B008545EC /* PermissionScheduleView.swift in Sources */, 6E4CB61F28D7902600116573 /* NSStyleKit.swift in Sources */, 6E21835D28D9516A00A622B3 /* CloudShare.swift in Sources */, 6E21831928D8116C00A622B3 /* NewKeyDocument.swift in Sources */, @@ -630,6 +644,8 @@ 6E169A7828D9A34F008545EC /* NavigationLink.swift in Sources */, 6E21836628D9516B00A622B3 /* CloudNewKeyInvitation.swift in Sources */, 6E21834128D8F3B500A622B3 /* Model.xcdatamodeld in Sources */, + 6E169A7F28D9C135008545EC /* NewPermissionView.swift in Sources */, + 6E169A7D28D9C097008545EC /* PermissionsView.swift in Sources */, 6E21833728D8F3B500A622B3 /* PersistentContainer.swift in Sources */, 6E21835E28D9516B00A622B3 /* CloudKey.swift in Sources */, 6E21836528D9516B00A622B3 /* CloudLock.swift in Sources */, @@ -1047,6 +1063,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 6E169A8228D9C25C008545EC /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/piknotech/SFSafeSymbols.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.0.0; + }; + }; 6E21830828D7FEA900A622B3 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MillerTechnologyPeru/KeychainAccess.git"; @@ -1074,6 +1098,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 6E169A8328D9C25C008545EC /* SFSafeSymbols */ = { + isa = XCSwiftPackageProductDependency; + package = 6E169A8228D9C25C008545EC /* XCRemoteSwiftPackageReference "SFSafeSymbols" */; + productName = SFSafeSymbols; + }; 6E21830928D7FEA900A622B3 /* KeychainAccess */ = { isa = XCSwiftPackageProductDependency; package = 6E21830828D7FEA900A622B3 /* XCRemoteSwiftPackageReference "KeychainAccess" */; diff --git a/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c8488a70..66f531c4 100644 --- a/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -45,6 +45,15 @@ "revision" : "ee878eaeb3efa09753a9e29e8deb8c331b96f3c8" } }, + { + "identity" : "sfsafesymbols", + "kind" : "remoteSourceControl", + "location" : "https://github.com/piknotech/SFSafeSymbols.git", + "state" : { + "revision" : "50bc33264e6c0972f905b61af656201cf6091de8", + "version" : "4.0.0" + } + }, { "identity" : "socket", "kind" : "remoteSourceControl", From f1b77afe2d327ce276c6b7ea08a814542a0980ec Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 11:49:04 -0700 Subject: [PATCH 094/229] [App] Added `PermissionsView` --- Sources/CoreLock/NewKey.swift | 2 +- Xcode/LockKit/Model/Store.swift | 6 +- Xcode/LockKit/Model/iCloud/CloudShare.swift | 2 +- Xcode/LockKit/View/EventsView.swift | 6 +- Xcode/LockKit/View/LockDetailView.swift | 4 +- Xcode/LockKit/View/PermissionsView.swift | 221 ++++++++++++++++++- Xcode/SmartLock/View/NearbyDevicesView.swift | 10 +- 7 files changed, 236 insertions(+), 15 deletions(-) diff --git a/Sources/CoreLock/NewKey.swift b/Sources/CoreLock/NewKey.swift index 98f28109..4bce9ce8 100644 --- a/Sources/CoreLock/NewKey.swift +++ b/Sources/CoreLock/NewKey.swift @@ -8,7 +8,7 @@ import Foundation /// New Key -public struct NewKey: Codable, Equatable, Hashable { +public struct NewKey: Codable, Equatable, Hashable, Identifiable { /// The unique identifier of the key. public let id: UUID diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 552a1ab2..e78ffc56 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -179,8 +179,8 @@ public extension Store { // update CoreData await updateCoreData() // update CloudKit - //do { try await uploadCloudApplicationData() } - //catch { log("⚠️ Unable to upload locks to iCloud") } + do { try await syncCloud() } + catch { log("⚠️ Unable to upload locks to iCloud") } } var applicationData: ApplicationData { @@ -574,7 +574,7 @@ public extension Store { id: lockCache.key.id, secret: keyData ) - + let context = backgroundContext // BLE request diff --git a/Xcode/LockKit/Model/iCloud/CloudShare.swift b/Xcode/LockKit/Model/iCloud/CloudShare.swift index 6eaa0253..94374c70 100644 --- a/Xcode/LockKit/Model/iCloud/CloudShare.swift +++ b/Xcode/LockKit/Model/iCloud/CloudShare.swift @@ -22,7 +22,7 @@ public enum CloudShare { case newKey = "com.colemancda.Lock.CloudKit.Share.NewKey" } - public struct NewKey: Codable, Equatable { + public struct NewKey: Codable, Equatable, Identifiable { /// Identifier public let id: ID diff --git a/Xcode/LockKit/View/EventsView.swift b/Xcode/LockKit/View/EventsView.swift index 597d8b5f..3c81bf73 100644 --- a/Xcode/LockKit/View/EventsView.swift +++ b/Xcode/LockKit/View/EventsView.swift @@ -14,9 +14,9 @@ public struct EventsView: View { public var store: Store @Environment(\.managedObjectContext) - var managedObjectContext + public var managedObjectContext - let lock: UUID? + public let lock: UUID? @FetchRequest( entity: EventManagedObject.entity(), @@ -26,7 +26,7 @@ public struct EventsView: View { predicate: nil, animation: .linear ) - var events: FetchedResults + private var events: FetchedResults private static let dateFormatter: DateFormatter = { let dateFormatter = DateFormatter() diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 36e269ed..368022a7 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -15,7 +15,7 @@ public struct LockDetailView: View { public var store: Store @Environment(\.managedObjectContext) - var managedObjectContext + public var managedObjectContext public let id: UUID @@ -250,7 +250,7 @@ extension LockDetailView { .font(.body) .foregroundColor(.gray) AppNavigationLink(destination: { - Text("Keys") + PermissionsView(id: id) }, label: { HStack { Text("\(keys) keys") diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index bd7fe197..9ed23936 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -6,15 +6,228 @@ // import SwiftUI +import CoreLock -struct PermissionsView: View { - var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) +public struct PermissionsView: View { + + @EnvironmentObject + public var store: Store + + @Environment(\.managedObjectContext) + public var managedObjectContext + + /// Identifier of lock + public let id: UUID + + @FetchRequest( + entity: KeyManagedObject.entity(), + sortDescriptors: [ + NSSortDescriptor(keyPath: \KeyManagedObject.created, ascending: false) + ], + predicate: nil, + animation: .linear + ) + private var keys: FetchedResults + + @FetchRequest( + entity: NewKeyManagedObject.entity(), + sortDescriptors: [ + NSSortDescriptor(keyPath: \NewKeyManagedObject.created, ascending: false) + ], + predicate: nil, + animation: .linear + ) + private var newKeys: FetchedResults + + fileprivate static let dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .short + dateFormatter.timeStyle = .none + return dateFormatter + }() + + fileprivate static let timeFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .none + dateFormatter.timeStyle = .short + return dateFormatter + }() + + fileprivate static let relativeDateTimeFormatter: RelativeDateTimeFormatter = { + let dateFormatter = RelativeDateTimeFormatter() + dateFormatter.dateTimeStyle = .numeric + dateFormatter.unitsStyle = .spellOut + return dateFormatter + }() + + public var body: some View { + StateView( + keys: keys.lazy.map { Key(managedObject: $0)! }, + newKeys: newKeys.lazy.map { NewKey(managedObject: $0)! }, + reload: reload + ) + .onAppear { + self.keys.nsPredicate = predicate + self.newKeys.nsPredicate = predicate + } + } + + public init(id: UUID) { + self.id = id + } +} + +private extension PermissionsView { + + var predicate: NSPredicate { + NSPredicate( + format: "%K == %@", + #keyPath(KeyManagedObject.lock.identifier), + id as NSUUID + ) + } + + func reload() { + + } +} + +internal extension PermissionsView { + + struct StateView : View where Keys: RandomAccessCollection, Keys.Element == Key, NewKeys: RandomAccessCollection, NewKeys.Element == NewKey { + + let keys: Keys + + let newKeys: NewKeys + + let reload: () -> () + + var body: some View { + List { + ForEach(keys) { + row(for: $0) + } + .onDelete(perform: deleteKey) + if newKeys.isEmpty == false { + Section("Pending") { + ForEach(newKeys) { + row(for: $0) + } + .onDelete(perform: deleteNewKey) + } + } + } + .navigationTitle("Permissions") + .refreshable { + reload() + } + .onAppear { + reload() + } + } } } +private extension PermissionsView.StateView { + + func row(for item: Key) -> some View { + AppNavigationLink(destination: { + destination(for: item) + }, label: { + LockRowView( + image: .permission(item.permission.type), + title: item.name, + subtitle: item.permission.localizedText, + trailing: ( + PermissionsView.dateFormatter.string(from: item.created), + PermissionsView.timeFormatter.string(from: item.created) + ) + ) + }) + } + + func row(for item: NewKey) -> some View { + AppNavigationLink(destination: { + destination(for: item) + }, label: { + LockRowView( + image: .permission(item.permission.type), + title: item.name, + subtitle: item.permission.localizedText + "\n" + "Expires " + PermissionsView.relativeDateTimeFormatter.localizedString(for: item.expiration, relativeTo: Date()), + trailing: ( + PermissionsView.dateFormatter.string(from: item.created), + PermissionsView.timeFormatter.string(from: item.created) + ) + ) + }) + } + + func destination(for item: Key) -> some View { + Text("Key") + } + + func destination(for item: NewKey) -> some View { + Text("Key") + } + + func deleteKey(at indexSet: IndexSet) { + + } + + func deleteNewKey(at indexSet: IndexSet) { + + } +} + +// MARK: - Preview + struct PermissionsView_Previews: PreviewProvider { static var previews: some View { - PermissionsView() + NavigationView { + PermissionsView.StateView( + keys: [ + Key( + id: UUID(), + name: "Owner", + created: Date() - 60 * 60 * 24, + permission: .owner + ), + Key( + id: UUID(), + name: "Key 1", + created: Date() - 60 * 60 * 6, + permission: .admin + ), + Key( + id: UUID(), + name: "Key 2", + created: Date() - 60 * 60 * 6, + permission: .anytime + ), + Key( + id: UUID(), + name: "Key 3", + created: Date() - 60 * 60 * 6, + permission: .scheduled( + Permission.Schedule( + expiry: Date() + 60 * 60 * 24 * 150, + interval: .default, + weekdays: [.monday, .tuesday, .wednesday, .thursday, .friday] + ) + ) + ) + ], + newKeys: [ + NewKey( + id: UUID(), + name: "Key 4", + permission: .anytime, + created: Date() - 60 * 60 * 2, + expiration: Date() + (60 * 60 * 24 * 1) + 10 + ) + ], + reload: { } + ) + } } } diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index 2a6c7439..7cff827f 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -28,7 +28,15 @@ struct NearbyDevicesView: View { ) .onAppear { if store.isScanning == false { - Task { await scan() } + Task { + try? await Task.sleep(timeInterval: 1.5) + await scan() + } + } + } + .onDisappear { + if store.isScanning { + store.stopScanning() } } } From 96c94628f62494558cce2b542ecf5672ed85e034 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 13:47:07 -0700 Subject: [PATCH 095/229] [App] Added `EventsView` preview --- .../Model/CoreData/LockManagedObject.swift | 4 + Xcode/LockKit/Model/Store.swift | 82 ++++------ Xcode/LockKit/View/EventsView.swift | 151 +++++++++++++++++- Xcode/LockKit/View/LockDetailView.swift | 4 +- 4 files changed, 186 insertions(+), 55 deletions(-) diff --git a/Xcode/LockKit/Model/CoreData/LockManagedObject.swift b/Xcode/LockKit/Model/CoreData/LockManagedObject.swift index 7aea9205..4af800da 100644 --- a/Xcode/LockKit/Model/CoreData/LockManagedObject.swift +++ b/Xcode/LockKit/Model/CoreData/LockManagedObject.swift @@ -67,8 +67,12 @@ internal extension NSManagedObjectContext { // insert locks return try locks.map { (identifier, cache) in if let managedObject = try find(id: identifier, type: LockManagedObject.self) { + // update name managedObject.name = cache.name + // update read info managedObject.update(information: cache.information, context: self) + // insert key + try insert(cache.key, for: managedObject) return managedObject } else { return LockManagedObject(id: identifier, diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index e78ffc56..e2d20f04 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -558,18 +558,19 @@ public extension Store { log("Confirmed new key for lock \(information.id)") } - @discardableResult func listKeys( - _ lock: NativeCentral.Peripheral, - notification updateBlock: ((KeysList, Bool) -> ()) = { _,_ in } - ) async throws -> Bool { + for peripheral: NativeCentral.Peripheral + ) async throws { // get lock key - guard let information = self.lockInformation[lock], - let lockCache = self[lock: information.id], - let keyData = self[key: lockCache.key.id] - else { return false } - + guard let information = self.lockInformation[peripheral] else { + throw LockError.unknownLock(peripheral) + } + let lockIdentifier = information.id + guard let lockCache = self[lock: lockIdentifier], + let keyData = self[key: lockCache.key.id] else { + throw LockError.noKey(lock: lockIdentifier) + } let key = KeyCredentials( id: lockCache.key.id, secret: keyData @@ -579,13 +580,10 @@ public extension Store { // BLE request let centralLog = central.log - try await central.connection(for: lock) { + try await central.connection(for: peripheral) { let stream = try await $0.listKeys(using: key, log: centralLog) - var list = KeysList() for try await notification in stream { - list.append(notification.key) // call completion block - updateBlock(list, notification.isLast) await context.commit { (context) in try context.insert(notification.key, for: information.id) } @@ -599,27 +597,22 @@ public extension Store { } log("Listed keys for lock \(information.id)") - - return true } - @discardableResult func listEvents( - _ lock: NativeCentral.Peripheral, - fetchRequest: LockEvent.FetchRequest? = nil, - notification updateBlock: @escaping ((EventsList, Bool) -> ()) = { _,_ in } - ) async throws -> Bool { - - var events = [LockEvent]() - + for peripheral: NativeCentral.Peripheral, + fetchRequest: LockEvent.FetchRequest? = nil + ) async throws { + // get lock key - guard let information = self.lockInformation[lock], - let lockCache = self[lock: information.id], - let keyData = self[key: lockCache.key.id] - else { return false } - + guard let information = self.lockInformation[peripheral] else { + throw LockError.unknownLock(peripheral) + } let lockIdentifier = information.id - + guard let lockCache = self[lock: lockIdentifier], + let keyData = self[key: lockCache.key.id] else { + throw LockError.noKey(lock: lockIdentifier) + } let key = KeyCredentials( id: lockCache.key.id, secret: keyData @@ -629,41 +622,28 @@ public extension Store { // BLE request let centralLog = central.log - try await central.connection(for: lock) { + try await central.connection(for: peripheral) { let stream = try await $0.listEvents(fetchRequest: fetchRequest, using: key, log: centralLog) for try await notification in stream { if let event = notification.event { - events.append(event) centralLog?("Recieved event \(event.id)") // store in CoreData await context.commit { (context) in try context.insert(event, for: information.id) } - } - // call completion block - updateBlock(events, notification.isLast) - - } - } - objectWillChange.send() - - // upload to iCloud - if preferences.isCloudBackupEnabled { - // perform concurrently - Task { - do { - for event in events { - let value = LockEvent.Cloud(event: event, for: lockIdentifier) - try await self.cloud.upload(value) + // upload to iCloud + if preferences.isCloudBackupEnabled { + // perform concurrently + Task { + let value = LockEvent.Cloud(event: event, for: lockIdentifier) + try await self.cloud.upload(value) + } } - } catch { - log("⚠️ Could not upload latest events to iCloud: \(error.localizedDescription)") } } } + objectWillChange.send() log("Listed events for lock \(information.id)") - - return true } } diff --git a/Xcode/LockKit/View/EventsView.swift b/Xcode/LockKit/View/EventsView.swift index 3c81bf73..ec766fbc 100644 --- a/Xcode/LockKit/View/EventsView.swift +++ b/Xcode/LockKit/View/EventsView.swift @@ -5,6 +5,8 @@ // Created by Alsey Coleman Miller on 9/19/22. // +import Foundation +import CoreData import SwiftUI import CoreLock @@ -105,7 +107,7 @@ private extension EventsView { if let peripheral = try await store.device(for: lock) { // load keys if admin if let permssion = store[lock: lock]?.key.permission, permssion.isAdministrator { - let _ = try await store.listKeys(peripheral) + try await store.listKeys(for: peripheral) } // load latest events var lastEventDate: Date? @@ -122,7 +124,7 @@ private extension EventsView { end: nil ) ) - let _ = try await store.listEvents(peripheral, fetchRequest: fetchRequest) + try await store.listEvents(for: peripheral, fetchRequest: fetchRequest) } } catch { log("⚠️ Error loading events for \(lock)") @@ -204,3 +206,148 @@ private extension EventsView { ) } } + +// MARK: - Preview + +struct EventsView_Previews: PreviewProvider { + + static var previews: some View { + PreviewView() + .environmentObject(Store.shared) + .environment(\.managedObjectContext, Store.shared.managedObjectContext) + } + + struct PreviewView: View { + + @EnvironmentObject + var store: Store + + @Environment(\.managedObjectContext) + var managedObjectContext + + @State + private var filter = false + + var body: some View { + NavigationView { + EventsView(lock: filter ? lock : nil) + .onAppear(perform: insertLockData) + .navigationBarItems( + leading: Button( + filter ? "Show All" : "Filter", + action: { filter.toggle() } + ), + trailing: Button( + "Insert", + action: insertNewEvents + ) + ) + } + } + } +} + +private extension EventsView_Previews.PreviewView { + + var lock: UUID { UUID(uuidString: "9C4CF5A6-A3A9-4C82-A5AF-62DDC9C1E5AE")! } + var ownerKey: UUID { UUID(uuidString: "43DF5757-93E3-436F-8D53-B6944FB5FF4C")! } + var setupEvent: UUID { UUID(uuidString: "3A2762BE-0189-402B-A72A-E8AED8B35962")! } + + func insertLockData() { + store.backgroundContext.commit { + // insert owner key + let key = Key( + id: ownerKey, + name: "Owner", + created: Date() - 6, + permission: .owner + ) + // insert lock + try $0.insert([ + lock: LockCache( + key: key, + name: "My lock", + information: .init( + buildVersion: .current, + version: .current, + status: .unlock, + unlockActions: [.default] + ) + ) + ]) + // insert setup event + (try $0.insert( + .setup( + LockEvent.Setup( + id: setupEvent, + date: key.created, + key: ownerKey + ) + ), for: lock + ) as! SetupEventManagedObject) + .date = key.created + } + } + + func insertNewEvents() { + insertLockData() + store.backgroundContext.commit { + // insert new events + let newKeyInvitation = UUID() + let key = Key( + id: UUID(), + name: "Key 2", + created: Date() - 4, + permission: .anytime + ) + try $0.insert(key, for: lock) + try $0.insert( + .unlock( + .init( + date: Date() - 5, + key: ownerKey, + action: .default + ) + ), for: lock + ) + + try $0.insert( + .createNewKey( + .init( + date: Date() - 4, + key: ownerKey, + newKey: newKeyInvitation + ) + ), for: lock + ) + try $0.insert( + .confirmNewKey( + .init( + date: Date() - 3, + newKey: newKeyInvitation, + key: key.id + ) + ), for: lock + ) + try $0.insert( + .removeKey( + .init( + date: Date() - 2, + key: ownerKey, + removedKey: UUID(), + type: .key + ) + ), for: lock + ) + try $0.insert( + .unlock( + .init( + date: Date() - 1, + key: key.id, + action: .default + ) + ), for: lock + ) + } + } +} diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 368022a7..12131153 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -106,7 +106,7 @@ private extension LockDetailView { let _ = try await store.readInformation(for: peripheral) // load keys if admin if let permssion = store[lock: lock]?.key.permission, permssion.isAdministrator { - let _ = try await store.listKeys(peripheral) + try await store.listKeys(for: peripheral) } // load latest events var lastEventDate: Date? @@ -123,7 +123,7 @@ private extension LockDetailView { end: nil ) ) - let _ = try await store.listEvents(peripheral, fetchRequest: fetchRequest) + let _ = try await store.listEvents(for: peripheral, fetchRequest: fetchRequest) } } catch { log("⚠️ Error loading information for \(lock)") From 73a88ce1c1f1480b20f88296cbc4ba777e34cee3 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 13:59:48 -0700 Subject: [PATCH 096/229] [App] Fixed macOS app --- Xcode/LockKit/View/EventsView.swift | 6 ++++++ Xcode/LockKit/View/PermissionScheduleView.swift | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Xcode/LockKit/View/EventsView.swift b/Xcode/LockKit/View/EventsView.swift index ec766fbc..52ea7394 100644 --- a/Xcode/LockKit/View/EventsView.swift +++ b/Xcode/LockKit/View/EventsView.swift @@ -209,6 +209,7 @@ private extension EventsView { // MARK: - Preview +#if DEBUG struct EventsView_Previews: PreviewProvider { static var previews: some View { @@ -229,6 +230,7 @@ struct EventsView_Previews: PreviewProvider { private var filter = false var body: some View { + #if os(iOS) NavigationView { EventsView(lock: filter ? lock : nil) .onAppear(perform: insertLockData) @@ -243,6 +245,9 @@ struct EventsView_Previews: PreviewProvider { ) ) } + #else + EventsView(lock: filter ? lock : nil) + #endif } } } @@ -351,3 +356,4 @@ private extension EventsView_Previews.PreviewView { } } } +#endif diff --git a/Xcode/LockKit/View/PermissionScheduleView.swift b/Xcode/LockKit/View/PermissionScheduleView.swift index 31aed4dc..322a085e 100644 --- a/Xcode/LockKit/View/PermissionScheduleView.swift +++ b/Xcode/LockKit/View/PermissionScheduleView.swift @@ -212,8 +212,10 @@ public struct PermissionScheduleView: View { } } } + #if os(iOS) .listStyle(GroupedListStyle()) .navigationBarTitle(Text("Schedule")) + #endif } } @@ -235,7 +237,7 @@ public extension PermissionScheduleView { public var schedule = Permission.Schedule() public var body: some View { - + #if os(iOS) NavigationView { PermissionScheduleView(schedule: $schedule) .navigationBarItems( @@ -249,6 +251,9 @@ public extension PermissionScheduleView { ) ) } + #else + PermissionScheduleView(schedule: $schedule) + #endif } } } From 65860f4733fbdf1d87ce91383c073a726a14c39a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 15:51:21 -0700 Subject: [PATCH 097/229] [App] Added `StackNavigationView` --- Xcode/SmartLock.xcodeproj/project.pbxproj | 4 + Xcode/SmartLock/View/SidebarView.swift | 88 +++++++++++++++---- .../SmartLock/View/StackNavigationView.swift | 72 +++++++++++++++ 3 files changed, 149 insertions(+), 15 deletions(-) create mode 100644 Xcode/SmartLock/View/StackNavigationView.swift diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index c4424fae..4d7576da 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -80,6 +80,7 @@ 6E4CB62128D7907200116573 /* PermissionIconViewUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62028D7907200116573 /* PermissionIconViewUIView.swift */; }; 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62228D7907E00116573 /* PermissionIconViewNSView.swift */; }; 6E4CB62528D792BF00116573 /* InfoPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62428D792BF00116573 /* InfoPlist.swift */; }; + 6E79A4BA28DA63FC00B44855 /* StackNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E79A4B828DA63C400B44855 /* StackNavigationView.swift */; }; 6EA7768528D7061600018FA3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA7768428D7061600018FA3 /* App.swift */; }; 6EA7768E28D7061600018FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6EA7768D28D7061600018FA3 /* Assets.xcassets */; }; 6EA7769228D7061600018FA3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6EA7769128D7061600018FA3 /* Preview Assets.xcassets */; }; @@ -184,6 +185,7 @@ 6E4CB62028D7907200116573 /* PermissionIconViewUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionIconViewUIView.swift; sourceTree = ""; }; 6E4CB62228D7907E00116573 /* PermissionIconViewNSView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionIconViewNSView.swift; sourceTree = ""; }; 6E4CB62428D792BF00116573 /* InfoPlist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InfoPlist.swift; path = ../../../iOS/SmartLock/InfoPlist.swift; sourceTree = ""; }; + 6E79A4B828DA63C400B44855 /* StackNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackNavigationView.swift; sourceTree = ""; }; 6EA7768128D7061600018FA3 /* SmartLock.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SmartLock.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6EA7768428D7061600018FA3 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 6EA7768D28D7061600018FA3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -299,6 +301,7 @@ 6E3276CF28D70C9400AF171B /* NearbyDevicesView.swift */, 6E2182EE28D7B37000A622B3 /* TabBarView.swift */, 6E21831A28D8341000A622B3 /* SidebarView.swift */, + 6E79A4B828DA63C400B44855 /* StackNavigationView.swift */, 6E21831E28D84A7300A622B3 /* SidebarLabel.swift */, 6E21831C28D834D200A622B3 /* ContentView.swift */, 6E2182F028D7B4FA00A622B3 /* SettingsView.swift */, @@ -604,6 +607,7 @@ files = ( 6E2182F128D7B4FA00A622B3 /* SettingsView.swift in Sources */, 6E4CB62528D792BF00116573 /* InfoPlist.swift in Sources */, + 6E79A4BA28DA63FC00B44855 /* StackNavigationView.swift in Sources */, 6E169A7928D9AA32008545EC /* Task.swift in Sources */, 6E21831D28D834D200A622B3 /* ContentView.swift in Sources */, 6EA7768528D7061600018FA3 /* App.swift in Sources */, diff --git a/Xcode/SmartLock/View/SidebarView.swift b/Xcode/SmartLock/View/SidebarView.swift index cfe8c3c4..7faf6034 100644 --- a/Xcode/SmartLock/View/SidebarView.swift +++ b/Xcode/SmartLock/View/SidebarView.swift @@ -15,7 +15,7 @@ struct SidebarView: View { var store: Store @State - private var selection: Item.ID? + private var sidebarSelection: Item.ID? @State private var isNearbyExpanded = true @@ -24,14 +24,20 @@ struct SidebarView: View { private var isKeysExpanded = true @State - private var detail: AnyView? + private var detail = AnyView(Text("Select a lock")) + + @State + private var currentSubview = AnyView(EmptyView()) + + @State + private var showingSubview = false var body: some View { SwiftUI.NavigationView { SidebarView.NavigationView( selection: Binding( - get: { selection }, - set: { selectionChanged($0) } + get: { sidebarSelection }, + set: { sidebarSelectionChanged($0) } ), isScanning: store.isScanning, locks: locks, @@ -42,13 +48,14 @@ struct SidebarView: View { ), isKeysExpanded: $isKeysExpanded ) - detail ?? AnyView(Text("Select a lock")) + detail } .navigationViewStyle(.columns) .onAppear { // configure navigation links AppNavigationLinkNavigate = { - self.detail = $0 + self.currentSubview = $0 + self.showingSubview = true } Task { do { try await Store.shared.syncCloud(conflicts: { _ in return true }) } // always override on macOS @@ -90,16 +97,67 @@ private extension SidebarView { } } - func selectionChanged(_ newValue: Item.ID?) { - selection = newValue - guard let _ = newValue else { - return // don't allow deselecting - } - detail = selectionDetail + func sidebarSelectionChanged(_ newValue: Item.ID?) { + sidebarSelection = newValue + // deselect Task { - try await Task.sleep(timeInterval: 0.2) - selection = nil + try? await Task.sleep(timeInterval: 0.5) + sidebarSelection = nil + } + guard let sidebarSelection = newValue else { + return // no effect if deselect } + guard let item = locks.first(where: { $0.id == sidebarSelection }) ?? keys.first(where: { $0.id == sidebarSelection }) else { + return + } + // try to show cached lock + switch item { + case let .lock(peripheralID, _, _): + guard let peripheral = store.peripherals.keys.first(where: { $0.id == peripheralID }) else { + return + } + guard let information = store.lockInformation[peripheral] else { + // cannot select loading locks + return + } + detail = detailView(for: information.id) + case let .key(keyID, _, _): + guard let lock = store.applicationData.locks.first(where: { $0.value.key.id == keyID })?.key else { + // invalid key selection + assertionFailure("Selected unknown key \(keyID)") + return + } + detail = detailView(for: lock) + } + } + + func detailView(for lock: UUID) -> AnyView { + AnyView(StackNavigationView( + currentSubview: $currentSubview, + showingSubview: $showingSubview, + rootView: { + LockDetailView(id: lock) + .toolbar { + ToolbarItem(placement: .navigation) { + Button(action: { + Task { + if store.isScanning { + store.stopScanning() + } else { + await store.scan() + } + } + }, label: { + if store.isScanning { + Label("stop", systemImage: "stop.fill") + } else { + Label("scan", systemImage: "arrow.clockwise") + } + }) + } + } + } + )) } func item(for peripheral: NativePeripheral) -> Item { @@ -124,7 +182,7 @@ private extension SidebarView { } var selectionDetail: AnyView { - guard let selection = self.selection, let item = locks.first(where: { $0.id == selection }) ?? keys.first(where: { $0.id == selection }) else { + guard let selection = self.sidebarSelection, let item = locks.first(where: { $0.id == selection }) ?? keys.first(where: { $0.id == selection }) else { return AnyView( Text("Select a lock") ) diff --git a/Xcode/SmartLock/View/StackNavigationView.swift b/Xcode/SmartLock/View/StackNavigationView.swift new file mode 100644 index 00000000..a2d48cd1 --- /dev/null +++ b/Xcode/SmartLock/View/StackNavigationView.swift @@ -0,0 +1,72 @@ +// +// StackNavigationView.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/20/22. +// + +#if os(macOS) +import SwiftUI + +/// Stack Navigation View for macOS +/// +/// https://betterprogramming.pub/stack-navigation-on-macos-41a40d8ec3a4 +struct StackNavigationView : View where RootContent: View { + + @Binding + var currentSubview: AnyView + + @Binding + var showingSubview: Bool + + let rootView: () -> RootContent + + var body: some View { + VStack { + if !showingSubview { + rootView() + } else { + StackNavigationSubview(isVisible: $showingSubview) { + currentSubview + } + .transition(.move(edge: .trailing)) + } + } + } + + init( + currentSubview: Binding, + showingSubview: Binding, + @ViewBuilder rootView: @escaping () -> RootContent) { + self._currentSubview = currentSubview + self._showingSubview = showingSubview + self.rootView = rootView + } +} + +private struct StackNavigationSubview: View where Content: View { + + @Binding + var isVisible: Bool + + let contentView: () -> Content + + var body: some View { + VStack { + contentView() // subview + } + .toolbar { + ToolbarItem(placement: .navigation) { + Button(action: { + withAnimation(.easeOut(duration: 0.3)) { + isVisible = false + } + }, label: { + Label("back", systemImage: "chevron.left") + }) + } + } + } +} + +#endif From c82baf6151a115620d9817be2410f42c0b3b6500 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 16:56:51 -0700 Subject: [PATCH 098/229] [App] Fixed macOS navigation --- Xcode/SmartLock/View/SidebarView.swift | 89 ++++++++++++++++---------- 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/Xcode/SmartLock/View/SidebarView.swift b/Xcode/SmartLock/View/SidebarView.swift index 7faf6034..1724c4cf 100644 --- a/Xcode/SmartLock/View/SidebarView.swift +++ b/Xcode/SmartLock/View/SidebarView.swift @@ -24,13 +24,7 @@ struct SidebarView: View { private var isKeysExpanded = true @State - private var detail = AnyView(Text("Select a lock")) - - @State - private var currentSubview = AnyView(EmptyView()) - - @State - private var showingSubview = false + private var navigationStack = [AnyView]() var body: some View { SwiftUI.NavigationView { @@ -48,14 +42,40 @@ struct SidebarView: View { ), isKeysExpanded: $isKeysExpanded ) - detail + switch navigationStack.count { + case 0: + Text("Select a lock") + case 1: + navigationStack[0] + case 2: + SwiftUI.NavigationView { + navigationStack[0] + .frame(minWidth: 350) + navigationStack[1] + .frame(minWidth: 350) + } + default: + SwiftUI.NavigationView { + navigationStack[0] + .frame(minWidth: 350) + navigationStack[1] + .frame(minWidth: 350) + navigationStack[2] + .frame(minWidth: 350) + } + .navigationViewStyle(.columns) + } } .navigationViewStyle(.columns) + .frame(minHeight: 550) .onAppear { // configure navigation links AppNavigationLinkNavigate = { - self.currentSubview = $0 - self.showingSubview = true + if navigationStack.count > 2 { + navigationStack[1] = navigationStack[2] + navigationStack[2] = $0 + } + self.navigationStack.append($0) } Task { do { try await Store.shared.syncCloud(conflicts: { _ in return true }) } // always override on macOS @@ -120,44 +140,43 @@ private extension SidebarView { // cannot select loading locks return } - detail = detailView(for: information.id) + navigationStack = [detailView(for: information.id)] case let .key(keyID, _, _): guard let lock = store.applicationData.locks.first(where: { $0.value.key.id == keyID })?.key else { // invalid key selection assertionFailure("Selected unknown key \(keyID)") return } - detail = detailView(for: lock) + navigationStack = [detailView(for: lock)] } } func detailView(for lock: UUID) -> AnyView { - AnyView(StackNavigationView( - currentSubview: $currentSubview, - showingSubview: $showingSubview, - rootView: { - LockDetailView(id: lock) - .toolbar { - ToolbarItem(placement: .navigation) { - Button(action: { - Task { - if store.isScanning { - store.stopScanning() - } else { - await store.scan() - } - } - }, label: { - if store.isScanning { - Label("stop", systemImage: "stop.fill") - } else { - Label("scan", systemImage: "arrow.clockwise") - } - }) + return AnyView( + LockDetailView(id: lock) + ) + + /* + .toolbar { + ToolbarItem(placement: .navigation) { + Button(action: { + Task { + if store.isScanning { + store.stopScanning() + } else { + await store.scan() } } + }, label: { + if store.isScanning { + Label("stop", systemImage: "stop.fill") + } else { + Label("scan", systemImage: "arrow.clockwise") + } + }) } - )) + } + */ } func item(for peripheral: NativePeripheral) -> Item { From bcb986aa8064c721a13158749b2dde4ef672d9e1 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 17:20:58 -0700 Subject: [PATCH 099/229] [App] Fixed macOS navigation --- Xcode/LockKit/View/LockDetailView.swift | 4 +-- Xcode/LockKit/View/NavigationLink.swift | 21 +++++++------- Xcode/LockKit/View/PermissionsView.swift | 4 +-- Xcode/SmartLock/View/KeysView.swift | 2 +- Xcode/SmartLock/View/NearbyDevicesView.swift | 2 +- Xcode/SmartLock/View/SidebarView.swift | 29 ++++++++++++-------- 6 files changed, 34 insertions(+), 28 deletions(-) diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 12131153..9d1cfc45 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -233,7 +233,7 @@ extension LockDetailView { .frame(width: titleWidth, height: nil, alignment: .leading) .font(.body) .foregroundColor(.gray) - AppNavigationLink(destination: { + AppNavigationLink(id: "events-\(id)", destination: { EventsView(lock: id) }, label: { HStack { @@ -249,7 +249,7 @@ extension LockDetailView { .frame(width: titleWidth, height: nil, alignment: .leading) .font(.body) .foregroundColor(.gray) - AppNavigationLink(destination: { + AppNavigationLink(id: "permissions-\(id)", destination: { PermissionsView(id: id) }, label: { HStack { diff --git a/Xcode/LockKit/View/NavigationLink.swift b/Xcode/LockKit/View/NavigationLink.swift index b33dfc1b..a808fd6c 100644 --- a/Xcode/LockKit/View/NavigationLink.swift +++ b/Xcode/LockKit/View/NavigationLink.swift @@ -7,25 +7,30 @@ import SwiftUI -#if os(macOS) +public var AppNavigationLinkNavigate: (String, AnyView) -> () = { _, _ in assertionFailure() } -public var AppNavigationLinkNavigate: (AnyView) -> () = { _ in assertionFailure() } - -public struct AppNavigationLink : View { +public struct AppNavigationLink : View { + + public let id: ID private let destination: Destination private let label: Label public var body: some View { + #if os(macOS) Button( action: buttonAction, label: { label } ) .buttonStyle(.plain) + #else + NavigationLink(destination: destination, label: label) + #endif } - public init(destination: () -> Destination, label: () -> Label) { + public init(id: ID, destination: () -> Destination, label: () -> Label) { + self.id = id self.destination = destination() self.label = label() } @@ -34,10 +39,6 @@ public struct AppNavigationLink : View { private extension AppNavigationLink { func buttonAction() { - AppNavigationLinkNavigate(AnyView(destination)) + AppNavigationLinkNavigate("\(id)", AnyView(destination)) } } - -#else -public typealias AppNavigationLink = SwiftUI.NavigationLink -#endif diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index 9ed23936..da76e7e7 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -131,7 +131,7 @@ internal extension PermissionsView { private extension PermissionsView.StateView { func row(for item: Key) -> some View { - AppNavigationLink(destination: { + AppNavigationLink(id: "key-\(item.id)", destination: { destination(for: item) }, label: { LockRowView( @@ -147,7 +147,7 @@ private extension PermissionsView.StateView { } func row(for item: NewKey) -> some View { - AppNavigationLink(destination: { + AppNavigationLink(id: "newKey-\(item.id)", destination: { destination(for: item) }, label: { LockRowView( diff --git a/Xcode/SmartLock/View/KeysView.swift b/Xcode/SmartLock/View/KeysView.swift index 50565544..dde3e47f 100644 --- a/Xcode/SmartLock/View/KeysView.swift +++ b/Xcode/SmartLock/View/KeysView.swift @@ -45,7 +45,7 @@ private extension KeysView.StateView { var list: some View { List(items) { (item) in - AppNavigationLink(destination: { + AppNavigationLink(id: "lock-\(item.id)", destination: { LockDetailView(id: item.id) }, label: { LockRowView(item) diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index 7cff827f..df47ad62 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -139,7 +139,7 @@ private extension NearbyDevicesView.StateView { case .loading, .unknown: LockRowView(item) case .key, .setup: - AppNavigationLink(destination: { + AppNavigationLink(id: "peripheral-\(item.id)", destination: { destination(item) }, label: { LockRowView(item) diff --git a/Xcode/SmartLock/View/SidebarView.swift b/Xcode/SmartLock/View/SidebarView.swift index 1724c4cf..28ece4c4 100644 --- a/Xcode/SmartLock/View/SidebarView.swift +++ b/Xcode/SmartLock/View/SidebarView.swift @@ -24,7 +24,7 @@ struct SidebarView: View { private var isKeysExpanded = true @State - private var navigationStack = [AnyView]() + private var navigationStack = [(id: String, view: AnyView)]() var body: some View { SwiftUI.NavigationView { @@ -46,21 +46,21 @@ struct SidebarView: View { case 0: Text("Select a lock") case 1: - navigationStack[0] + navigationStack[0].view case 2: SwiftUI.NavigationView { - navigationStack[0] + navigationStack[0].view .frame(minWidth: 350) - navigationStack[1] + navigationStack[1].view .frame(minWidth: 350) } default: SwiftUI.NavigationView { - navigationStack[0] + navigationStack[0].view .frame(minWidth: 350) - navigationStack[1] + navigationStack[1].view .frame(minWidth: 350) - navigationStack[2] + navigationStack[2].view .frame(minWidth: 350) } .navigationViewStyle(.columns) @@ -70,12 +70,16 @@ struct SidebarView: View { .frame(minHeight: 550) .onAppear { // configure navigation links - AppNavigationLinkNavigate = { + AppNavigationLinkNavigate = { (id, view) in + guard navigationStack.contains(where: { $0.id == id }) == false else { + return + } if navigationStack.count > 2 { navigationStack[1] = navigationStack[2] - navigationStack[2] = $0 + navigationStack[2] = (id, view) + } else { + self.navigationStack.append((id, view)) } - self.navigationStack.append($0) } Task { do { try await Store.shared.syncCloud(conflicts: { _ in return true }) } // always override on macOS @@ -140,14 +144,15 @@ private extension SidebarView { // cannot select loading locks return } - navigationStack = [detailView(for: information.id)] + let lock = information.id + navigationStack = [("lock-\(lock)", detailView(for: lock))] case let .key(keyID, _, _): guard let lock = store.applicationData.locks.first(where: { $0.value.key.id == keyID })?.key else { // invalid key selection assertionFailure("Selected unknown key \(keyID)") return } - navigationStack = [detailView(for: lock)] + navigationStack = [("lock-\(lock)", detailView(for: lock))] } } From 8487169b27aae6aded98c8259fae4c55afaad8a0 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 17:56:12 -0700 Subject: [PATCH 100/229] [App] Added `AppNavigationLinkID` --- Xcode/LockKit/View/LockDetailView.swift | 4 +- Xcode/LockKit/View/NavigationLink.swift | 48 ++++++++++++++++++-- Xcode/LockKit/View/PermissionsView.swift | 8 ++-- Xcode/SmartLock/View/KeysView.swift | 2 +- Xcode/SmartLock/View/NearbyDevicesView.swift | 2 +- Xcode/SmartLock/View/SidebarView.swift | 32 ++++++++----- 6 files changed, 73 insertions(+), 23 deletions(-) diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 9d1cfc45..591de503 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -233,7 +233,7 @@ extension LockDetailView { .frame(width: titleWidth, height: nil, alignment: .leading) .font(.body) .foregroundColor(.gray) - AppNavigationLink(id: "events-\(id)", destination: { + AppNavigationLink(id: .events(id), destination: { EventsView(lock: id) }, label: { HStack { @@ -249,7 +249,7 @@ extension LockDetailView { .frame(width: titleWidth, height: nil, alignment: .leading) .font(.body) .foregroundColor(.gray) - AppNavigationLink(id: "permissions-\(id)", destination: { + AppNavigationLink(id: .permissions(id), destination: { PermissionsView(id: id) }, label: { HStack { diff --git a/Xcode/LockKit/View/NavigationLink.swift b/Xcode/LockKit/View/NavigationLink.swift index a808fd6c..cb401f3b 100644 --- a/Xcode/LockKit/View/NavigationLink.swift +++ b/Xcode/LockKit/View/NavigationLink.swift @@ -7,9 +7,11 @@ import SwiftUI -public var AppNavigationLinkNavigate: (String, AnyView) -> () = { _, _ in assertionFailure() } +public var AppNavigationLinkNavigate: (AppNavigationLink.ID, AnyView) -> () = { _, _ in assertionFailure() } -public struct AppNavigationLink : View { +public struct AppNavigationLink : View { + + public typealias ID = AppNavigationLinkID public let id: ID @@ -39,6 +41,46 @@ public struct AppNavigationLink : private extension AppNavigationLink { func buttonAction() { - AppNavigationLinkNavigate("\(id)", AnyView(destination)) + AppNavigationLinkNavigate(id, AnyView(destination)) + } +} + +public enum AppNavigationLinkID: Hashable { + + case lock(UUID) + case events(UUID) + case permissions(UUID) + case key(UUID, pending: Bool = false) + case keySchedule(UUID) + + static func newKey(_ id: UUID) -> AppNavigationLinkID { + .key(id, pending: true) + } +} + +public enum AppNavigationLinkType: String { + + case lock + case events + case permissions + case key + case keySchedule +} + +public extension AppNavigationLinkID { + + var type: AppNavigationLinkType { + switch self { + case .lock: + return .lock + case .events: + return .events + case .permissions: + return .permissions + case .key: + return .key + case .keySchedule: + return .keySchedule + } } } diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index da76e7e7..4c630c53 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -131,7 +131,7 @@ internal extension PermissionsView { private extension PermissionsView.StateView { func row(for item: Key) -> some View { - AppNavigationLink(id: "key-\(item.id)", destination: { + AppNavigationLink(id: .key(item.id, pending: false), destination: { destination(for: item) }, label: { LockRowView( @@ -147,7 +147,7 @@ private extension PermissionsView.StateView { } func row(for item: NewKey) -> some View { - AppNavigationLink(id: "newKey-\(item.id)", destination: { + AppNavigationLink(id: .newKey(item.id), destination: { destination(for: item) }, label: { LockRowView( @@ -163,11 +163,11 @@ private extension PermissionsView.StateView { } func destination(for item: Key) -> some View { - Text("Key") + Text("Key \(item.id)") } func destination(for item: NewKey) -> some View { - Text("Key") + Text("New Key \(item.id)") } func deleteKey(at indexSet: IndexSet) { diff --git a/Xcode/SmartLock/View/KeysView.swift b/Xcode/SmartLock/View/KeysView.swift index dde3e47f..8b28cc38 100644 --- a/Xcode/SmartLock/View/KeysView.swift +++ b/Xcode/SmartLock/View/KeysView.swift @@ -45,7 +45,7 @@ private extension KeysView.StateView { var list: some View { List(items) { (item) in - AppNavigationLink(id: "lock-\(item.id)", destination: { + NavigationLink(destination: { LockDetailView(id: item.id) }, label: { LockRowView(item) diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index df47ad62..2e4f9533 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -139,7 +139,7 @@ private extension NearbyDevicesView.StateView { case .loading, .unknown: LockRowView(item) case .key, .setup: - AppNavigationLink(id: "peripheral-\(item.id)", destination: { + NavigationLink(destination: { destination(item) }, label: { LockRowView(item) diff --git a/Xcode/SmartLock/View/SidebarView.swift b/Xcode/SmartLock/View/SidebarView.swift index 28ece4c4..651fe188 100644 --- a/Xcode/SmartLock/View/SidebarView.swift +++ b/Xcode/SmartLock/View/SidebarView.swift @@ -24,7 +24,7 @@ struct SidebarView: View { private var isKeysExpanded = true @State - private var navigationStack = [(id: String, view: AnyView)]() + private var navigationStack = [(id: AppNavigationLinkID, view: AnyView)]() var body: some View { SwiftUI.NavigationView { @@ -71,15 +71,7 @@ struct SidebarView: View { .onAppear { // configure navigation links AppNavigationLinkNavigate = { (id, view) in - guard navigationStack.contains(where: { $0.id == id }) == false else { - return - } - if navigationStack.count > 2 { - navigationStack[1] = navigationStack[2] - navigationStack[2] = (id, view) - } else { - self.navigationStack.append((id, view)) - } + navigate(id: id, view: view) } Task { do { try await Store.shared.syncCloud(conflicts: { _ in return true }) } // always override on macOS @@ -121,6 +113,22 @@ private extension SidebarView { } } + func navigate(id: AppNavigationLinkID, view: AnyView) { + //guard navigationStack.contains(where: { $0.id == id }) == false else { + // return + //} + + // try to replace existing of same type + if let index = navigationStack.firstIndex(where: { $0.id.type == id.type }) { + navigationStack[index] = (id, view) + } else if navigationStack.count > 2 { + navigationStack[1] = navigationStack[2] // push stack, max 3 + navigationStack[2] = (id, view) + } else { + navigationStack.append((id, view)) // push stack + } + } + func sidebarSelectionChanged(_ newValue: Item.ID?) { sidebarSelection = newValue // deselect @@ -145,14 +153,14 @@ private extension SidebarView { return } let lock = information.id - navigationStack = [("lock-\(lock)", detailView(for: lock))] + navigationStack = [(.lock(lock), detailView(for: lock))] case let .key(keyID, _, _): guard let lock = store.applicationData.locks.first(where: { $0.value.key.id == keyID })?.key else { // invalid key selection assertionFailure("Selected unknown key \(keyID)") return } - navigationStack = [("lock-\(lock)", detailView(for: lock))] + navigationStack = [(.lock(lock), detailView(for: lock))] } } From ad7ed64f015504361606b7ea17b9fe5566d1f7d0 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 18:16:06 -0700 Subject: [PATCH 101/229] [App] Show pending keys count in Lock Detail --- Xcode/LockKit/View/LockDetailView.swift | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 591de503..8c6d505a 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -27,6 +27,7 @@ public struct LockDetailView: View { cache: cache, events: events, keys: keys, + newKeys: newKeys, unlock: unlock ) .refreshable { @@ -94,6 +95,16 @@ private extension LockDetailView { return (try? managedObjectContext.count(for: fetchRequest)) ?? 0 } + var newKeys: Int { + let fetchRequest = NewKeyManagedObject.fetchRequest() + fetchRequest.predicate = NSPredicate( + format: "%K == %@", + #keyPath(NewKeyManagedObject.lock.identifier), + id as NSUUID + ) + return (try? managedObjectContext.count(for: fetchRequest)) ?? 0 + } + func reload() { let lock = self.id Task { @@ -144,6 +155,8 @@ extension LockDetailView { let keys: Int + let newKeys: Int + @State var showID = false @@ -253,7 +266,11 @@ extension LockDetailView { PermissionsView(id: id) }, label: { HStack { - Text("\(keys) keys") + if newKeys > 0 { + Text("\(keys) keys, \(newKeys) pending") + } else { + Text("\(keys) keys") + } Image(systemName: "chevron.right") } }) @@ -311,6 +328,7 @@ struct LockDetailView_Previews: PreviewProvider { ), events: 10, keys: 2, + newKeys: 5, unlock: { } ) } From b886c0c4c4b1498ddf951cd68f9d126ba2d97731 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 18:29:52 -0700 Subject: [PATCH 102/229] [App] Fixed macOS navigation stack --- Xcode/LockKit/View/PermissionScheduleView.swift | 6 ++++++ Xcode/LockKit/View/PermissionsView.swift | 12 ++++++++++-- Xcode/SmartLock/View/SidebarView.swift | 11 +++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Xcode/LockKit/View/PermissionScheduleView.swift b/Xcode/LockKit/View/PermissionScheduleView.swift index 322a085e..6117958b 100644 --- a/Xcode/LockKit/View/PermissionScheduleView.swift +++ b/Xcode/LockKit/View/PermissionScheduleView.swift @@ -17,10 +17,12 @@ public struct PermissionScheduleView: View { // MARK: - Properties public init(schedule: Permission.Schedule = .init()) { + isEditable = false _schedule = Binding(get: { schedule }, set: { _ in }) // read only } public init(schedule: Binding) { + isEditable = true _schedule = schedule } @@ -29,6 +31,8 @@ public struct PermissionScheduleView: View { @Binding public var schedule: Permission.Schedule + private let isEditable: Bool + @State private var defaultExpiration = Date() + (60 * 60 * 24) @@ -212,6 +216,8 @@ public struct PermissionScheduleView: View { } } } + .disabled(isEditable == false) + .padding(20) #if os(iOS) .listStyle(GroupedListStyle()) .navigationBarTitle(Text("Schedule")) diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index 4c630c53..df3b7ae2 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -163,11 +163,19 @@ private extension PermissionsView.StateView { } func destination(for item: Key) -> some View { - Text("Key \(item.id)") + AppNavigationLink(id: .keySchedule(item.id), destination: { + PermissionScheduleView(schedule: item.permission.schedule ?? Permission.Schedule()) + }, label: { + Text("Key \(item.id)") + }) } func destination(for item: NewKey) -> some View { - Text("New Key \(item.id)") + AppNavigationLink(id: .keySchedule(item.id), destination: { + PermissionScheduleView(schedule: item.permission.schedule ?? Permission.Schedule()) + }, label: { + Text("New Key \(item.id)") + }) } func deleteKey(at indexSet: IndexSet) { diff --git a/Xcode/SmartLock/View/SidebarView.swift b/Xcode/SmartLock/View/SidebarView.swift index 651fe188..345fa572 100644 --- a/Xcode/SmartLock/View/SidebarView.swift +++ b/Xcode/SmartLock/View/SidebarView.swift @@ -118,6 +118,17 @@ private extension SidebarView { // return //} + // special cases + switch id.type { + case .lock: + navigationStack = [(id, view)] + case .permissions, + .events: + navigationStack = (navigationStack.first.flatMap { [$0] } ?? []) + [(id, view)] + default: + break + } + // try to replace existing of same type if let index = navigationStack.firstIndex(where: { $0.id.type == id.type }) { navigationStack[index] = (id, view) From 68f6a4c4601193b08190f883748014045035ce65 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 18:30:10 -0700 Subject: [PATCH 103/229] [CoreLock] Added `Permission.schedule` --- Sources/CoreLock/Permission.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Sources/CoreLock/Permission.swift b/Sources/CoreLock/Permission.swift index 888c0b10..ae662597 100644 --- a/Sources/CoreLock/Permission.swift +++ b/Sources/CoreLock/Permission.swift @@ -141,6 +141,16 @@ public extension Permission { } } +public extension Permission { + + var schedule: Schedule? { + guard case let .scheduled(schedule) = self else { + return nil + } + return schedule + } +} + // MARK: - Schedule Interval public extension Permission.Schedule { From a1cc63cbcc6a21b5cb198d81da51222df3505a00 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 18:38:21 -0700 Subject: [PATCH 104/229] [App] Fixed `AppNavigationLink` for iOS --- Xcode/LockKit/View/NavigationLink.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Xcode/LockKit/View/NavigationLink.swift b/Xcode/LockKit/View/NavigationLink.swift index cb401f3b..fcf6e8d3 100644 --- a/Xcode/LockKit/View/NavigationLink.swift +++ b/Xcode/LockKit/View/NavigationLink.swift @@ -27,7 +27,7 @@ public struct AppNavigationLink : View { ) .buttonStyle(.plain) #else - NavigationLink(destination: destination, label: label) + NavigationLink(destination: destination, label: { label }) #endif } From fc91cd3a93949a0954894723ef9a5887d32f40f2 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 19:29:42 -0700 Subject: [PATCH 105/229] [CoreLock] Added `Permission.Schedule.Weekdays.workdays` --- Sources/CoreLock/Permission.swift | 66 ++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/Sources/CoreLock/Permission.swift b/Sources/CoreLock/Permission.swift index ae662597..8eac9c2c 100644 --- a/Sources/CoreLock/Permission.swift +++ b/Sources/CoreLock/Permission.swift @@ -217,25 +217,53 @@ public extension Permission.Schedule { public extension Permission.Schedule.Weekdays { - static let all = Permission.Schedule.Weekdays( - sunday: true, - monday: true, - tuesday: true, - wednesday: true, - thursday: true, - friday: true, - saturday: true - ) - - static let none = Permission.Schedule.Weekdays( - sunday: false, - monday: false, - tuesday: false, - wednesday: false, - thursday: false, - friday: false, - saturday: false - ) + static var all: Permission.Schedule.Weekdays { + Permission.Schedule.Weekdays( + sunday: true, + monday: true, + tuesday: true, + wednesday: true, + thursday: true, + friday: true, + saturday: true + ) + } + + static var none: Permission.Schedule.Weekdays { + Permission.Schedule.Weekdays( + sunday: false, + monday: false, + tuesday: false, + wednesday: false, + thursday: false, + friday: false, + saturday: false + ) + } + + static var workdays: Permission.Schedule.Weekdays { + Permission.Schedule.Weekdays( + sunday: false, + monday: true, + tuesday: true, + wednesday: true, + thursday: true, + friday: true, + saturday: false + ) + } + + static var weekend: Permission.Schedule.Weekdays { + Permission.Schedule.Weekdays( + sunday: true, + monday: false, + tuesday: false, + wednesday: false, + thursday: false, + friday: false, + saturday: true + ) + } } public extension Permission.Schedule.Weekdays { From f89aadc02142c339b16d4f5aa4a6baf72f8f903e Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 19:40:27 -0700 Subject: [PATCH 106/229] [App] Added `KeyDetailView` --- Xcode/LockKit/View/KeyDetailView.swift | 204 ++++++++++++++++++ .../LockKit/View/PermissionScheduleView.swift | 11 +- Xcode/LockKit/View/PermissionsView.swift | 12 +- Xcode/SmartLock.xcodeproj/project.pbxproj | 4 + Xcode/SmartLock/View/SidebarView.swift | 2 +- 5 files changed, 218 insertions(+), 15 deletions(-) create mode 100644 Xcode/LockKit/View/KeyDetailView.swift diff --git a/Xcode/LockKit/View/KeyDetailView.swift b/Xcode/LockKit/View/KeyDetailView.swift new file mode 100644 index 00000000..1b2458e4 --- /dev/null +++ b/Xcode/LockKit/View/KeyDetailView.swift @@ -0,0 +1,204 @@ +// +// KeyDetailView.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/20/22. +// + +import SwiftUI +import CoreLock + +public struct KeyDetailView: View { + + public let key: Value + + @State + var showID = false + + private static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .short + return formatter + }() + + private var titleWidth: CGFloat { + 100 + } + + public init(key: Value) { + self.key = key + } + + public var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + HStack { + Spacer() + PermissionIconView(permission: key.permission.type) + .frame(width: 150, height: 150, alignment: .center) + .padding(30) + Spacer() + } + VStack(alignment: .leading, spacing: 20) { + // info + if showID { + HStack { + Text("Key") + .frame(width: titleWidth, height: nil, alignment: .leading) + .font(.body) + .foregroundColor(.gray) + Text(verbatim: key.id.description) + } + } + HStack { + Text("Type") + .frame(width: titleWidth, height: nil, alignment: .leading) + .font(.body) + .foregroundColor(.gray) + if let schedule = key.permission.schedule { + AppNavigationLink(id: .keySchedule(key.id), destination: { + PermissionScheduleView(schedule: schedule) + }, label: { + HStack { + Text(verbatim: key.permission.localizedText) + .foregroundColor(.primary) + Image(systemName: "chevron.right") + } + }) + } else { + Text(verbatim: key.permission.localizedText) + .foregroundColor(.primary) + } + } + HStack { + Text("Created") + .frame(width: titleWidth, height: nil, alignment: .leading) + .font(.body) + .foregroundColor(.gray) + Text(verbatim: Self.dateFormatter.string(from: key.created)) + } + if let expiration = key.expiration { + HStack { + Text("Expiration") + .frame(width: titleWidth, height: nil, alignment: .leading) + .font(.body) + .foregroundColor(.gray) + Text(verbatim: Self.dateFormatter.string(from: expiration)) + } + } + } + } + .padding(20) + .buttonStyle(.plain) + } + .navigationTitle(Text(verbatim: key.name)) + } +} + +// MARK: - Supporting Types + +public extension KeyDetailView { + + enum Value: Equatable, Hashable { + case key(Key) + case newKey(NewKey) + } +} + +extension KeyDetailView.Value: Identifiable { + + public var id: UUID { + switch self { + case .key(let key): + return key.id + case .newKey(let newKey): + return newKey.id + } + } +} + +public extension KeyDetailView.Value { + + var name: String { + switch self { + case .key(let key): + return key.name + case .newKey(let newKey): + return newKey.name + } + } + + var created: Date { + switch self { + case .key(let key): + return key.created + case .newKey(let newKey): + return newKey.created + } + } + + var permission: Permission { + switch self { + case .key(let key): + return key.permission + case .newKey(let newKey): + return newKey.permission + } + } + + var expiration: Date? { + switch self { + case .key: + return nil + case .newKey(let newKey): + return newKey.expiration + } + } +} + +// MARK: - Preview + +#if DEBUG +struct KeyDetailView_Previews: PreviewProvider { + + static let keys: [KeyDetailView.Value] = [ + .key( + Key( + id: UUID(), + name: "Owner", + created: Date() - 60 * 60 * 24, + permission: .owner + ) + ), + .newKey( + NewKey( + id: UUID(), + name: "New Key", + permission: .scheduled( + Permission.Schedule( + expiry: Date() + 60 * 60 * 24 * 90, + interval: .default, + weekdays: .workdays + ) + ), + created: Date() - 60 * 60 * 2, + expiration: Date() + 60 * 60 * 25 + ) + ) + ] + + static var previews: some View { + Group { + ForEach(keys) { key in + NavigationView { + KeyDetailView( + key: key + ) + } + .previewDisplayName("\(key.name) Key") + } + } + } +} +#endif diff --git a/Xcode/LockKit/View/PermissionScheduleView.swift b/Xcode/LockKit/View/PermissionScheduleView.swift index 6117958b..17e35de5 100644 --- a/Xcode/LockKit/View/PermissionScheduleView.swift +++ b/Xcode/LockKit/View/PermissionScheduleView.swift @@ -216,11 +216,14 @@ public struct PermissionScheduleView: View { } } } - .disabled(isEditable == false) .padding(20) - #if os(iOS) - .listStyle(GroupedListStyle()) - .navigationBarTitle(Text("Schedule")) + .navigationTitle("Schedule") + .disabled(isEditable == false) + #if os(macOS) + //.buttonStyle(isEditable ? .plain : .bordered) + #elseif os(iOS) + //.listStyle(GroupedListStyle()) + //.listStyle(.plain) #endif } } diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index df3b7ae2..046427d5 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -163,19 +163,11 @@ private extension PermissionsView.StateView { } func destination(for item: Key) -> some View { - AppNavigationLink(id: .keySchedule(item.id), destination: { - PermissionScheduleView(schedule: item.permission.schedule ?? Permission.Schedule()) - }, label: { - Text("Key \(item.id)") - }) + KeyDetailView(key: .key(item)) } func destination(for item: NewKey) -> some View { - AppNavigationLink(id: .keySchedule(item.id), destination: { - PermissionScheduleView(schedule: item.permission.schedule ?? Permission.Schedule()) - }, label: { - Text("New Key \(item.id)") - }) + KeyDetailView(key: .newKey(item)) } func deleteKey(at indexSet: IndexSet) { diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 4d7576da..e4f50d4d 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -81,6 +81,7 @@ 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62228D7907E00116573 /* PermissionIconViewNSView.swift */; }; 6E4CB62528D792BF00116573 /* InfoPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62428D792BF00116573 /* InfoPlist.swift */; }; 6E79A4BA28DA63FC00B44855 /* StackNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E79A4B828DA63C400B44855 /* StackNavigationView.swift */; }; + 6E9E37AB28DAA6D100BE7128 /* KeyDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */; }; 6EA7768528D7061600018FA3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA7768428D7061600018FA3 /* App.swift */; }; 6EA7768E28D7061600018FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6EA7768D28D7061600018FA3 /* Assets.xcassets */; }; 6EA7769228D7061600018FA3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6EA7769128D7061600018FA3 /* Preview Assets.xcassets */; }; @@ -186,6 +187,7 @@ 6E4CB62228D7907E00116573 /* PermissionIconViewNSView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionIconViewNSView.swift; sourceTree = ""; }; 6E4CB62428D792BF00116573 /* InfoPlist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InfoPlist.swift; path = ../../../iOS/SmartLock/InfoPlist.swift; sourceTree = ""; }; 6E79A4B828DA63C400B44855 /* StackNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackNavigationView.swift; sourceTree = ""; }; + 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyDetailView.swift; sourceTree = ""; }; 6EA7768128D7061600018FA3 /* SmartLock.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SmartLock.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6EA7768428D7061600018FA3 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 6EA7768D28D7061600018FA3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -353,6 +355,7 @@ 6E169A7C28D9C097008545EC /* PermissionsView.swift */, 6E169A7E28D9C135008545EC /* NewPermissionView.swift */, 6E169A8028D9C15B008545EC /* PermissionScheduleView.swift */, + 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */, ); path = View; sourceTree = ""; @@ -632,6 +635,7 @@ 6E21830528D7C51900A622B3 /* Log.swift in Sources */, 6E21833B28D8F3B500A622B3 /* ConfirmNewKeyEventManagedObject.swift in Sources */, 6E21830C28D7FEF600A622B3 /* FileManager.swift in Sources */, + 6E9E37AB28DAA6D100BE7128 /* KeyDetailView.swift in Sources */, 6E4CB61028D7867700116573 /* LockRowView.swift in Sources */, 6E21835028D9506100A622B3 /* Preferences.swift in Sources */, 6E21831828D8116C00A622B3 /* NewKey.swift in Sources */, diff --git a/Xcode/SmartLock/View/SidebarView.swift b/Xcode/SmartLock/View/SidebarView.swift index 345fa572..7e90df5d 100644 --- a/Xcode/SmartLock/View/SidebarView.swift +++ b/Xcode/SmartLock/View/SidebarView.swift @@ -63,7 +63,7 @@ struct SidebarView: View { navigationStack[2].view .frame(minWidth: 350) } - .navigationViewStyle(.columns) + } } .navigationViewStyle(.columns) From 270fbc98302980c35303be9ba590ff0de88fff48 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 20:09:20 -0700 Subject: [PATCH 107/229] [App] Fixed reloading `PermissionsView` --- Xcode/LockKit/View/KeyDetailView.swift | 42 +++++++++++++------ .../LockKit/View/PermissionScheduleView.swift | 2 +- Xcode/LockKit/View/PermissionsView.swift | 29 ++++++++++++- 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/Xcode/LockKit/View/KeyDetailView.swift b/Xcode/LockKit/View/KeyDetailView.swift index 1b2458e4..123b4cb3 100644 --- a/Xcode/LockKit/View/KeyDetailView.swift +++ b/Xcode/LockKit/View/KeyDetailView.swift @@ -162,6 +162,19 @@ public extension KeyDetailView.Value { #if DEBUG struct KeyDetailView_Previews: PreviewProvider { + static var previews: some View { + Group { + ForEach(keys) { key in + NavigationView { + KeyDetailView( + key: key + ) + } + .previewDisplayName(key.name) + } + } + } + static let keys: [KeyDetailView.Value] = [ .key( Key( @@ -171,6 +184,22 @@ struct KeyDetailView_Previews: PreviewProvider { permission: .owner ) ), + .key( + Key( + id: UUID(), + name: "Key 2", + created: Date() - 60 * 60 * 24, + permission: .admin + ) + ), + .key( + Key( + id: UUID(), + name: "Key 3", + created: Date() - 60 * 60 * 24, + permission: .anytime + ) + ), .newKey( NewKey( id: UUID(), @@ -187,18 +216,5 @@ struct KeyDetailView_Previews: PreviewProvider { ) ) ] - - static var previews: some View { - Group { - ForEach(keys) { key in - NavigationView { - KeyDetailView( - key: key - ) - } - .previewDisplayName("\(key.name) Key") - } - } - } } #endif diff --git a/Xcode/LockKit/View/PermissionScheduleView.swift b/Xcode/LockKit/View/PermissionScheduleView.swift index 17e35de5..cde19065 100644 --- a/Xcode/LockKit/View/PermissionScheduleView.swift +++ b/Xcode/LockKit/View/PermissionScheduleView.swift @@ -218,8 +218,8 @@ public struct PermissionScheduleView: View { } .padding(20) .navigationTitle("Schedule") - .disabled(isEditable == false) #if os(macOS) + .disabled(isEditable == false) //.buttonStyle(isEditable ? .plain : .bordered) #elseif os(iOS) //.listStyle(GroupedListStyle()) diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index 046427d5..7e0dc61c 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -70,6 +70,15 @@ public struct PermissionsView: View { self.keys.nsPredicate = predicate self.newKeys.nsPredicate = predicate } + .toolbar { + ToolbarItem(placement: .primaryAction) { + if let key = store[lock: id]?.key, key.permission.isAdministrator { + Button(action: newPermission, label: { + Image(systemSymbol: .plus) + }) + } + } + } } public init(id: UUID) { @@ -88,7 +97,25 @@ private extension PermissionsView { } func reload() { - + Task { + guard await store.central.state == .poweredOn else { + return + } + do { + if store.isScanning { + store.stopScanning() + } + guard let peripheral = try await store.device(for: id) else { + // unable to find device + return + } + try await store.listKeys(for: peripheral) + } + } + } + + func newPermission() { + // show modal } } From eb06e1b797ca374ffcbb08fbe8741caba906f565 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 20:31:47 -0700 Subject: [PATCH 108/229] [App] Updated logging --- Xcode/LockKit/Model/Store.swift | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index e2d20f04..8660738d 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -577,12 +577,21 @@ public extension Store { ) let context = backgroundContext - + var keysCount = 0 + var newKeysCount = 0 // BLE request let centralLog = central.log try await central.connection(for: peripheral) { let stream = try await $0.listKeys(using: key, log: centralLog) for try await notification in stream { + switch notification.key { + case let .key(key): + keysCount += 1 + centralLog?("Recieved \(key.permission.type) key \(key.id) \(key.name)") + case let .newKey(key): + newKeysCount += 1 + centralLog?("Recieved \(key.permission.type) key \(key.id) \(key.name)") + } // call completion block await context.commit { (context) in try context.insert(notification.key, for: information.id) @@ -596,7 +605,7 @@ public extension Store { //updateCloud() } - log("Listed keys for lock \(information.id)") + log("Recieved \(keysCount) keys and \(newKeysCount) pending keys for lock \(information.id)") } func listEvents( @@ -619,14 +628,15 @@ public extension Store { ) let context = backgroundContext - + var eventsCount = 0 // BLE request let centralLog = central.log try await central.connection(for: peripheral) { let stream = try await $0.listEvents(fetchRequest: fetchRequest, using: key, log: centralLog) for try await notification in stream { if let event = notification.event { - centralLog?("Recieved event \(event.id)") + centralLog?("Recieved \(event.type) event \(event.id)") + eventsCount += 1 // store in CoreData await context.commit { (context) in try context.insert(event, for: information.id) @@ -644,6 +654,6 @@ public extension Store { } objectWillChange.send() - log("Listed events for lock \(information.id)") + log("Recieved \(eventsCount) events for lock \(information.id)") } } From c48b6556c94da353b618658e11f111dabe8603e8 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 20:48:24 -0700 Subject: [PATCH 109/229] [App] Fixed saving `Key` in CoreData --- .../LockKit/Model/CoreData/KeyManagedObject.swift | 13 +++++++++++-- .../Model/CoreData/NewKeyManagedObject.swift | 15 ++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Xcode/LockKit/Model/CoreData/KeyManagedObject.swift b/Xcode/LockKit/Model/CoreData/KeyManagedObject.swift index 55121b3b..839e1c4a 100644 --- a/Xcode/LockKit/Model/CoreData/KeyManagedObject.swift +++ b/Xcode/LockKit/Model/CoreData/KeyManagedObject.swift @@ -15,12 +15,20 @@ public final class KeyManagedObject: NSManagedObject { internal convenience init(_ value: Key, lock: LockManagedObject, context: NSManagedObjectContext) { self.init(context: context) self.identifier = value.id + self.update(value, lock: lock, context: context) + } + + internal func update(_ value: Key, lock: LockManagedObject, context: NSManagedObjectContext) { self.lock = lock self.name = value.name self.created = value.created self.permission = numericCast(value.permission.type.rawValue) if case let .scheduled(schedule) = value.permission { - self.schedule = .init(schedule, context: context) + if let _ = self.schedule { + // don't update + } else { + self.schedule = .init(schedule, context: context) + } } } } @@ -40,7 +48,7 @@ public extension Key { case .owner: permission = .owner case .admin: - permission = .owner + permission = .admin case .anytime: permission = .anytime case .scheduled: @@ -71,6 +79,7 @@ internal extension NSManagedObjectContext { if let managedObject = try find(id: key.id, type: KeyManagedObject.self) { assert(managedObject.lock == lock, "Key stored with conflicting lock") + managedObject.update(key, lock: lock, context: self) return managedObject } else { return KeyManagedObject(key, lock: lock, context: self) diff --git a/Xcode/LockKit/Model/CoreData/NewKeyManagedObject.swift b/Xcode/LockKit/Model/CoreData/NewKeyManagedObject.swift index 5d58ee62..1e77a70d 100644 --- a/Xcode/LockKit/Model/CoreData/NewKeyManagedObject.swift +++ b/Xcode/LockKit/Model/CoreData/NewKeyManagedObject.swift @@ -15,14 +15,22 @@ public final class NewKeyManagedObject: NSManagedObject { internal convenience init(_ value: NewKey, lock: LockManagedObject, context: NSManagedObjectContext) { self.init(context: context) self.identifier = value.id + self.update(value, lock: lock, context: context) + } + + internal func update(_ value: NewKey, lock: LockManagedObject, context: NSManagedObjectContext) { self.lock = lock self.name = value.name self.created = value.created + self.expiration = value.expiration self.permission = numericCast(value.permission.type.rawValue) if case let .scheduled(schedule) = value.permission { - self.schedule = .init(schedule, context: context) + if let _ = self.schedule { + // don't update + } else { + self.schedule = .init(schedule, context: context) + } } - self.expiration = value.expiration } } @@ -42,7 +50,7 @@ public extension NewKey { case .owner: permission = .owner case .admin: - permission = .owner + permission = .admin case .anytime: permission = .anytime case .scheduled: @@ -74,6 +82,7 @@ internal extension NSManagedObjectContext { if let managedObject = try find(id: newKey.id, type: NewKeyManagedObject.self) { assert(managedObject.lock == lock, "Key stored with conflicting lock") + managedObject.update(newKey, lock: lock, context: self) return managedObject } else { return NewKeyManagedObject(newKey, lock: lock, context: self) From d2ba0e800049a1333f9c7f61bb4deaf56d707149 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 22:41:16 -0700 Subject: [PATCH 110/229] [App] Added `NewPermissionView` --- Xcode/LockKit/View/NewPermissionView.swift | 256 +++++++++++++++++- Xcode/LockKit/View/PermissionsView.swift | 20 +- ...wKeySelectPermissionViewController.strings | 2 +- 3 files changed, 271 insertions(+), 7 deletions(-) diff --git a/Xcode/LockKit/View/NewPermissionView.swift b/Xcode/LockKit/View/NewPermissionView.swift index ba0b205f..ba7c964f 100644 --- a/Xcode/LockKit/View/NewPermissionView.swift +++ b/Xcode/LockKit/View/NewPermissionView.swift @@ -6,15 +6,263 @@ // import SwiftUI +import CoreLock -struct NewPermissionView: View { - var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) +public struct NewPermissionView: View { + + @EnvironmentObject + public var store: Store + + @Environment(\.managedObjectContext) + public var managedObjectContext + + public let id: UUID + + @State + private var permission: Permission = .anytime + + @State + private var name: String = "" + + @State + private var state: ViewState = .editing + + public var body: some View { + StateView( + permission: $permission, + name: $name, + state: $state, + create: create + ) + } + + public init( + id: UUID, + name: String = "", + permission: Permission = .anytime + ) { + self.id = id + self.name = name + self.permission = permission + } +} + +private extension NewPermissionView { + + func create() { + state = .loading + let permission = self.permission + let name = self.name.isEmpty ? "\(permission.type.localizedText) Key" : self.name + #if targetEnvironment(simulator) + Task { + try? await Task.sleep(timeInterval: 1) + state = .error("Unable to connect to device.") + } + #else + Task { + do { + guard await store.central.state == .poweredOn else { + throw LockError.bluetoothUnavailable + } + if store.isScanning { + store.stopScanning() + } + guard let peripheral = try await store.device(for: id) else { + throw LockError.notInRange(lock: id) + } + let newKey = try await store.newKey( + for: peripheral, + permission: permission, + name: name + ) + state = .editing + shareKey(newKey) + } catch { + state = .error(error.localizedDescription) + } + } + #endif + } + + func shareKey(_ newKey: NewKey.Invitation) { + + } +} + +internal extension NewPermissionView { + + enum ViewState: Equatable, Hashable { + case editing + case loading + case error(String) + } +} + +internal extension NewPermissionView { + + struct StateView: View { + + @Binding + var permission: Permission + + @Binding + var name: String + + @Binding + var state: ViewState + + let create: () -> () + + var body: some View { + form + .disabled(state == .loading) + .navigationTitle("New Key") + .toolbar { + createToolbarItem + } + } + } +} + +private extension NewPermissionView.StateView { + + var permissionTypes: [PermissionType] { + [.admin, .anytime, .scheduled] + } + + var form: some View { + Form { + if case let .error(error) = state { + Section("") { + Text(verbatim: "⚠️ " + error) + } + } + nameSection + permissionSection + } + } + + var createToolbarItem: some ToolbarContent { + ToolbarItem(placement: .primaryAction) { + if state == .loading { + AnyView( + ProgressView() + .progressViewStyle(.circular) + ) + } else { + AnyView( + Button("Create", action: { create() }) + ) + } + } + } + + var nameSection: some View { + Section(header: Text("Name")) { + TextField("New Key", text: $name) + } + } + + var permissionSection: some View { + Section(header: Text("Permission")) { + ForEach(permissionTypes, id: \.rawValue) { type in + if type == .scheduled { + NavigationLink(destination: { + PermissionScheduleView( + schedule: Binding(get: { + // default schedule is the same as anytime + return permission.schedule ?? Permission.Schedule() + }, set: { + // schedule must be customized + if $0 == .init() { + permission = .anytime + } else { + permission = .scheduled($0) + } + }) + ) + }, label: { + NewPermissionView.PermissionTypeView( + permission: type, + isSelected: self.permission.type == type + ) + }) + } else { + Button(action: { + switch type { + case .admin: + permission = .admin + case .anytime: + permission = .anytime + default: + assertionFailure() + } + }, label: { + NewPermissionView.PermissionTypeView( + permission: type, + isSelected: self.permission.type == type + ) + }) + .buttonStyle(.plain) + } + } + } + } +} + +internal extension NewPermissionView { + + struct PermissionTypeView: View { + + let permission: PermissionType + + let isSelected: Bool + + var body: some View { + HStack(alignment: .center, spacing: 8) { + LockRowView( + image: .permission(permission), + title: permission.localizedText, + subtitle: descriptionText + ) + if isSelected { + Image(systemSymbol: .checkmark) + .frame(width: selectionInset) + } else { + Spacer(minLength: selectionInset) + } + } + } + } +} + +private extension NewPermissionView.PermissionTypeView { + + var selectionInset: CGFloat { + 25 + } + + var descriptionText: String { + switch permission { + case .admin: + return "Admin keys have unlimited access, and can create new keys." + case .anytime: + return "Anytime keys have unlimited access, but cannot create new keys." + case .scheduled: + return "Scheduled keys have limited access during specified hours, and expire at a certain date. New keys cannot be created from this key." + case .owner: + assertionFailure("Cannot create owner keys") + return "Cannot create owner keys" + } } } +#if DEBUG struct NewPermissionView_Previews: PreviewProvider { static var previews: some View { - NewPermissionView() + NavigationView { + NewPermissionView(id: UUID()) + } } } +#endif diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index 7e0dc61c..a2a8d2b6 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -60,6 +60,9 @@ public struct PermissionsView: View { return dateFormatter }() + @State + private var showNewKeyModal = false + public var body: some View { StateView( keys: keys.lazy.map { Key(managedObject: $0)! }, @@ -72,13 +75,18 @@ public struct PermissionsView: View { } .toolbar { ToolbarItem(placement: .primaryAction) { - if let key = store[lock: id]?.key, key.permission.isAdministrator { + if canCreateNewKey { Button(action: newPermission, label: { Image(systemSymbol: .plus) }) } } } + .sheet(isPresented: $showNewKeyModal, onDismiss: { }) { + NavigationView { + NewPermissionView(id: id) + } + } } public init(id: UUID) { @@ -114,8 +122,16 @@ private extension PermissionsView { } } + var canCreateNewKey: Bool { + #if targetEnvironment(simulator) + return true + #else + return store[lock: id]?.key.permission.isAdministrator ?? false + #endif + } + func newPermission() { - // show modal + showNewKeyModal = true } } diff --git a/iOS/LockKit/Localized Strings/en.lproj/NewKeySelectPermissionViewController.strings b/iOS/LockKit/Localized Strings/en.lproj/NewKeySelectPermissionViewController.strings index fe4b287e..339e9a99 100644 --- a/iOS/LockKit/Localized Strings/en.lproj/NewKeySelectPermissionViewController.strings +++ b/iOS/LockKit/Localized Strings/en.lproj/NewKeySelectPermissionViewController.strings @@ -8,4 +8,4 @@ "Admin.Description" = "Admin keys have unlimited access, and can create new keys."; "Anytime.Description" = "Anytime keys have unlimited access, but cannot create new keys."; -"Scheduled.Description" = "Scheduled keys have limited access during specified hours, and expire at a certain date. New keys cannot be created from this key"; +"Scheduled.Description" = "Scheduled keys have limited access during specified hours, and expire at a certain date. New keys cannot be created from this key."; From be773f1e74cba2e4d38b9ead1c1e283950d7894c Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 23:24:24 -0700 Subject: [PATCH 111/229] [App] Fixed `NewPermissionView` for macOS --- Xcode/LockKit/View/NewPermissionView.swift | 84 ++++++++++++++-------- Xcode/LockKit/View/PermissionsView.swift | 36 +++++++++- 2 files changed, 90 insertions(+), 30 deletions(-) diff --git a/Xcode/LockKit/View/NewPermissionView.swift b/Xcode/LockKit/View/NewPermissionView.swift index ba7c964f..67a49a8d 100644 --- a/Xcode/LockKit/View/NewPermissionView.swift +++ b/Xcode/LockKit/View/NewPermissionView.swift @@ -18,6 +18,8 @@ public struct NewPermissionView: View { public let id: UUID + private var completion: (NewKey.Invitation) -> () = { _ in } + @State private var permission: Permission = .anytime @@ -27,23 +29,25 @@ public struct NewPermissionView: View { @State private var state: ViewState = .editing - public var body: some View { - StateView( - permission: $permission, - name: $name, - state: $state, - create: create - ) - } - public init( id: UUID, name: String = "", - permission: Permission = .anytime + permission: Permission = .anytime, + completion: @escaping (NewKey.Invitation) -> () ) { self.id = id self.name = name self.permission = permission + self.completion = completion + } + + public var body: some View { + StateView( + permission: $permission, + name: $name, + state: $state, + create: create + ) } } @@ -56,7 +60,9 @@ private extension NewPermissionView { #if targetEnvironment(simulator) Task { try? await Task.sleep(timeInterval: 1) - state = .error("Unable to connect to device.") + if self.name.isEmpty { + state = .error("Unable to connect to device.") + } } #else Task { @@ -76,17 +82,13 @@ private extension NewPermissionView { name: name ) state = .editing - shareKey(newKey) + completion(newKey) } catch { state = .error(error.localizedDescription) } } #endif } - - func shareKey(_ newKey: NewKey.Invitation) { - - } } internal extension NewPermissionView { @@ -139,11 +141,17 @@ private extension NewPermissionView.StateView { } nameSection permissionSection + #if os(macOS) + Spacer(minLength: 20) + PermissionScheduleView( + schedule: schedule + ) + #endif } } var createToolbarItem: some ToolbarContent { - ToolbarItem(placement: .primaryAction) { + ToolbarItem(placement: .confirmationAction) { if state == .loading { AnyView( ProgressView() @@ -167,19 +175,20 @@ private extension NewPermissionView.StateView { Section(header: Text("Permission")) { ForEach(permissionTypes, id: \.rawValue) { type in if type == .scheduled { + #if os(macOS) + Button(action: { + permission = .scheduled(.init(interval: .default)) + }, label: { + NewPermissionView.PermissionTypeView( + permission: type, + isSelected: self.permission.type == type + ) + }) + .buttonStyle(.plain) + #else NavigationLink(destination: { PermissionScheduleView( - schedule: Binding(get: { - // default schedule is the same as anytime - return permission.schedule ?? Permission.Schedule() - }, set: { - // schedule must be customized - if $0 == .init() { - permission = .anytime - } else { - permission = .scheduled($0) - } - }) + schedule: schedule ) }, label: { NewPermissionView.PermissionTypeView( @@ -187,6 +196,7 @@ private extension NewPermissionView.StateView { isSelected: self.permission.type == type ) }) + #endif } else { Button(action: { switch type { @@ -208,6 +218,20 @@ private extension NewPermissionView.StateView { } } } + + var schedule: Binding { + Binding(get: { + // default schedule is the same as anytime + return permission.schedule ?? Permission.Schedule() + }, set: { + // schedule must be customized + if $0 == .init() { + permission = .anytime + } else { + permission = .scheduled($0) + } + }) + } } internal extension NewPermissionView { @@ -261,7 +285,9 @@ private extension NewPermissionView.PermissionTypeView { struct NewPermissionView_Previews: PreviewProvider { static var previews: some View { NavigationView { - NewPermissionView(id: UUID()) + NewPermissionView(id: UUID()) { _ in + + } } } } diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index a2a8d2b6..b9befd22 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -83,9 +83,31 @@ public struct PermissionsView: View { } } .sheet(isPresented: $showNewKeyModal, onDismiss: { }) { + #if os(iOS) NavigationView { - NewPermissionView(id: id) + NewPermissionView(id: id, completion: didCreateNewKey) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + showNewKeyModal = false + } + } + } + } + #elseif os(macOS) + ScrollView { + NewPermissionView(id: id, completion: didCreateNewKey) + .padding(30) + } + .frame(width: 500, height: 500) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + showNewKeyModal = false + } + } } + #endif } } @@ -133,6 +155,18 @@ private extension PermissionsView { func newPermission() { showNewKeyModal = true } + + func didCreateNewKey(_ newKey: NewKey.Invitation) { + showNewKeyModal = false + Task { + try? await Task.sleep(timeInterval: 0.5) + #if os(iOS) + + #elseif os(macOS) + + #endif + } + } } internal extension PermissionsView { From 86e36167bbd0df228d34f06ca4ae976a598c1104 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 20 Sep 2022 23:46:12 -0700 Subject: [PATCH 112/229] [App] Added `ActivityView` --- Xcode/LockKit/View/UIKit/ActivityView.swift | 151 ++++++++++++++++++++ Xcode/SmartLock.xcodeproj/project.pbxproj | 4 + 2 files changed, 155 insertions(+) create mode 100644 Xcode/LockKit/View/UIKit/ActivityView.swift diff --git a/Xcode/LockKit/View/UIKit/ActivityView.swift b/Xcode/LockKit/View/UIKit/ActivityView.swift new file mode 100644 index 00000000..b221872f --- /dev/null +++ b/Xcode/LockKit/View/UIKit/ActivityView.swift @@ -0,0 +1,151 @@ +// +// ActivityView.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/20/22. +// + +#if os(iOS) +import SwiftUI +import UIKit + +public struct ActivityView: UIViewControllerRepresentable { + + public let activityItems: [Any] + + public let applicationActivities: [UIActivity]? + + public let excludedActivityTypes: [UIActivity.ActivityType]? + + public init( + activityItems: [Any], + applicationActivities: [UIActivity]? = nil, + excludedActivityTypes: [UIActivity.ActivityType]? = nil + ) { + self.activityItems = activityItems + self.applicationActivities = applicationActivities + self.excludedActivityTypes = excludedActivityTypes + } + + public func makeUIViewController(context: Context) -> UIActivityViewController { + let controller = UIActivityViewController( + activityItems: activityItems, + applicationActivities: applicationActivities + ) + controller.excludedActivityTypes = excludedActivityTypes + return controller + } + + public func updateUIViewController(_ viewController: UIActivityViewController, context: Context) { + + + } +} + +// MARK: - Supporting Types + +public struct ShareSheetContextMenuModifer: ViewModifier { + + @State + private var showShareSheet: Bool = false + + public let activityItems: [Any] + + public let applicationActivities: [UIActivity]? + + public let excludedActivityTypes: [UIActivity.ActivityType]? + + public init( + activityItems: [Any], + applicationActivities: [UIActivity]? = nil, + excludedActivityTypes: [UIActivity.ActivityType]? = nil + ) { + self.activityItems = activityItems + self.applicationActivities = applicationActivities + self.excludedActivityTypes = excludedActivityTypes + } + + public func body(content: Content) -> some View { + content + .contextMenu { + Button(action: { + self.showShareSheet.toggle() + }) { + Text("Share") + Image(systemName: "square.and.arrow.up") + } + } + .sheet(isPresented: $showShareSheet, content: { + ActivityView( + activityItems: activityItems, + applicationActivities: applicationActivities, + excludedActivityTypes: excludedActivityTypes + ) + }) + } +} + +public extension View { + + func shareSheetContextMenu( + activityItems: [Any], + applicationActivities: [UIActivity]? = nil, + excludedActivityTypes: [UIActivity.ActivityType]? = nil + ) -> some View { + self.modifier( + ShareSheetContextMenuModifer( + activityItems: activityItems, + applicationActivities: applicationActivities, + excludedActivityTypes: excludedActivityTypes + ) + ) + } +} + +// MARK: - Preview + +struct ActivityView_Previews: PreviewProvider { + + static var previews: some View { + Group { + PreviewView( + activityItems: [URL(string: "http://google.com")! as NSURL] + ) + } + } + + struct PreviewView: View { + + let activityItems: [Any] + + let applicationActivities: [UIActivity]? = nil + + let excludedActivityTypes: [UIActivity.ActivityType]? = nil + + @State + var showShareSheet = false + + var body: some View { + NavigationView { + Button("Share") { + showShareSheet = true + } + .padding(20) + .navigationTitle("Share Sheet") + .shareSheetContextMenu( + activityItems: activityItems, + applicationActivities: applicationActivities, + excludedActivityTypes: excludedActivityTypes + ) + .sheet(isPresented: $showShareSheet, content: { + ActivityView( + activityItems: activityItems, + applicationActivities: applicationActivities, + excludedActivityTypes: excludedActivityTypes + ) + }) + } + } + } +} +#endif diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index e4f50d4d..0834d333 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -82,6 +82,7 @@ 6E4CB62528D792BF00116573 /* InfoPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62428D792BF00116573 /* InfoPlist.swift */; }; 6E79A4BA28DA63FC00B44855 /* StackNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E79A4B828DA63C400B44855 /* StackNavigationView.swift */; }; 6E9E37AB28DAA6D100BE7128 /* KeyDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */; }; + 6E9E37AD28DADF2600BE7128 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AC28DADF2600BE7128 /* ActivityView.swift */; }; 6EA7768528D7061600018FA3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA7768428D7061600018FA3 /* App.swift */; }; 6EA7768E28D7061600018FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6EA7768D28D7061600018FA3 /* Assets.xcassets */; }; 6EA7769228D7061600018FA3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6EA7769128D7061600018FA3 /* Preview Assets.xcassets */; }; @@ -188,6 +189,7 @@ 6E4CB62428D792BF00116573 /* InfoPlist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InfoPlist.swift; path = ../../../iOS/SmartLock/InfoPlist.swift; sourceTree = ""; }; 6E79A4B828DA63C400B44855 /* StackNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackNavigationView.swift; sourceTree = ""; }; 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyDetailView.swift; sourceTree = ""; }; + 6E9E37AC28DADF2600BE7128 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; 6EA7768128D7061600018FA3 /* SmartLock.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SmartLock.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6EA7768428D7061600018FA3 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 6EA7768D28D7061600018FA3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -367,6 +369,7 @@ 6E4CB62028D7907200116573 /* PermissionIconViewUIView.swift */, 6E21830228D7C47500A622B3 /* Appearance.swift */, 6ED248CB28D7A3BF00F78D07 /* ActivityIndicatorView.swift */, + 6E9E37AC28DADF2600BE7128 /* ActivityView.swift */, ); path = UIKit; sourceTree = ""; @@ -659,6 +662,7 @@ 6E21836528D9516B00A622B3 /* CloudLock.swift in Sources */, 6E21834228D8F3B500A622B3 /* UnlockEventManagedObject.swift in Sources */, 6E21833528D8F3B500A622B3 /* SetupEventManagedObject.swift in Sources */, + 6E9E37AD28DADF2600BE7128 /* ActivityView.swift in Sources */, 6E21834328D8F3B500A622B3 /* ManagedObject.swift in Sources */, 6E21834028D8F3B500A622B3 /* RemoveKeyEventManagedObject.swift in Sources */, 6E21834A28D90F7A00A622B3 /* LocalAuthentication.swift in Sources */, From 7be52991e88144227fc965b47b7d7a8f36059f80 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 21 Sep 2022 00:10:46 -0700 Subject: [PATCH 113/229] [App] Added `NewKeyFileActivityItem` --- Xcode/LockKit/View/PermissionsView.swift | 27 +++-- Xcode/LockKit/View/UIKit/ActivityItem.swift | 107 ++++++++++++++++++++ Xcode/LockKit/View/UIKit/ActivityView.swift | 29 +++++- Xcode/SmartLock.xcodeproj/project.pbxproj | 4 + 4 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 Xcode/LockKit/View/UIKit/ActivityItem.swift diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index b9befd22..4b47b284 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -63,6 +63,9 @@ public struct PermissionsView: View { @State private var showNewKeyModal = false + @State + private var newKeyInvitation: NewKey.Invitation? + public var body: some View { StateView( keys: keys.lazy.map { Key(managedObject: $0)! }, @@ -109,6 +112,22 @@ public struct PermissionsView: View { } #endif } + #if os(iOS) + .sheet(isPresented: Binding(get: { + newKeyInvitation != nil + }, set: { + if $0 == false { + newKeyInvitation = nil + } + }), content: { + ActivityView( + activityItems: newKeyInvitation + .flatMap { [NewKeyFileActivityItem(invitation: $0)] as [Any] } ?? [], + applicationActivities: nil, + excludedActivityTypes: NewKeyFileActivityItem.excludedActivityTypes + ) + }) + #endif } public init(id: UUID) { @@ -159,12 +178,8 @@ private extension PermissionsView { func didCreateNewKey(_ newKey: NewKey.Invitation) { showNewKeyModal = false Task { - try? await Task.sleep(timeInterval: 0.5) - #if os(iOS) - - #elseif os(macOS) - - #endif + try? await Task.sleep(timeInterval: 1) + self.newKeyInvitation = newKey } } } diff --git a/Xcode/LockKit/View/UIKit/ActivityItem.swift b/Xcode/LockKit/View/UIKit/ActivityItem.swift new file mode 100644 index 00000000..9e0aa38d --- /dev/null +++ b/Xcode/LockKit/View/UIKit/ActivityItem.swift @@ -0,0 +1,107 @@ +// +// ActivityItem.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/20/22. +// + +#if os(iOS) +import Foundation +import UIKit + +public final class NewKeyFileActivityItem: UIActivityItemProvider { + + public init(invitation: NewKey.Invitation) { + self.invitation = invitation + + let url = type(of: self).url(for: invitation) + do { try FileManager.default.removeItem(at: url) } + catch { } // ignore + super.init(placeholderItem: url) + } + + private static func url(for invitation: NewKey.Invitation) -> URL { + let fileName = invitation.key.name + "." + NewKey.Invitation.fileExtension + let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName) + return fileURL + } + + public let invitation: NewKey.Invitation + + public lazy var fileURL = type(of: self).url(for: invitation) + + private lazy var encoder = JSONEncoder() + + /// Generate the actual item. + public override var item: Any { + // save invitation file + let url = type(of: self).url(for: invitation) + do { + let data = try encoder.encode(invitation) + try data.write(to: url, options: [.atomic]) + return url + } catch { + assertionFailure("Could not create key file: \(error)") + return url + } + } + + // MARK: - UIActivityItemSource + + public override func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String { + + return invitation.key.name + } + + public override func activityViewController(_ activityViewController: UIActivityViewController, thumbnailImageForActivityType activityType: UIActivity.ActivityType?, suggestedSize size: CGSize) -> UIImage? { + return nil//return UIImage(permissionType: invitation.key.permission.type) + } + /* + @available(iOSApplicationExtension 13.0, *) + public override func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? { + + let permissionImageURL = AssetExtractor.shared.url(for: invitation.key.permission.type.image) + assert(permissionImageURL != nil, "Missing permission image") + let metadata = LPLinkMetadata() + metadata.title = invitation.key.name + metadata.imageProvider = permissionImageURL.flatMap { NSItemProvider(contentsOf: $0) } + return metadata + }*/ +} + +public extension NewKeyFileActivityItem { + + static let excludedActivityTypes: [UIActivity.ActivityType] = [.postToTwitter, + .postToFacebook, + .postToWeibo, + .postToTencentWeibo, + .postToFlickr, + .postToVimeo, + .print, + .assignToContact, + .saveToCameraRoll, + .addToReadingList, + .openInIBooks, + .markupAsPDF] +} + +// MARK: - Activity Type + +/// `UIActivity` types +public enum LockActivity: String { + + case newKey = "com.colemancda.lock.activity.newKey" + case manageKeys = "com.colemancda.lock.activity.manageKeys" + case delete = "com.colemancda.lock.activity.delete" + case rename = "com.colemancda.lock.activity.rename" + case update = "com.colemancda.lock.activity.update" + case homeKitEnable = "com.colemancda.lock.activity.homeKitEnable" + case addVoiceShortcut = "com.colemancda.lock.activity.addVoiceShortcut" + case shareKeyCloudKit = "com.colemancda.lock.activity.shareKeyCloudKit" + + var activityType: UIActivity.ActivityType { + return UIActivity.ActivityType(rawValue: self.rawValue) + } +} + +#endif diff --git a/Xcode/LockKit/View/UIKit/ActivityView.swift b/Xcode/LockKit/View/UIKit/ActivityView.swift index b221872f..f2f1a161 100644 --- a/Xcode/LockKit/View/UIKit/ActivityView.swift +++ b/Xcode/LockKit/View/UIKit/ActivityView.swift @@ -9,9 +9,12 @@ import SwiftUI import UIKit +/// A view controller that you use to offer standard services from your app. +/// +/// Wraps `UIActivityViewController` for SwiftUI public struct ActivityView: UIViewControllerRepresentable { - public let activityItems: [Any] + public var activityItems: [Any] public let applicationActivities: [UIActivity]? @@ -36,14 +39,34 @@ public struct ActivityView: UIViewControllerRepresentable { return controller } - public func updateUIViewController(_ viewController: UIActivityViewController, context: Context) { - + public func updateUIViewController( + _ viewController: UIActivityViewController, + context: Context + ) { } } // MARK: - Supporting Types +public extension View { + + func shareSheet( + isPresented: Binding, + activityItems: [Any], + applicationActivities: [UIActivity]? = nil, + excludedActivityTypes: [UIActivity.ActivityType]? = nil + ) -> some View { + self.sheet(isPresented: isPresented, content: { + ActivityView( + activityItems: activityItems, + applicationActivities: applicationActivities, + excludedActivityTypes: excludedActivityTypes + ) + }) + } +} + public struct ShareSheetContextMenuModifer: ViewModifier { @State diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 0834d333..eb00e54f 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -83,6 +83,7 @@ 6E79A4BA28DA63FC00B44855 /* StackNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E79A4B828DA63C400B44855 /* StackNavigationView.swift */; }; 6E9E37AB28DAA6D100BE7128 /* KeyDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */; }; 6E9E37AD28DADF2600BE7128 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AC28DADF2600BE7128 /* ActivityView.swift */; }; + 6E9E37AF28DAEBE000BE7128 /* ActivityItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AE28DAEBE000BE7128 /* ActivityItem.swift */; }; 6EA7768528D7061600018FA3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA7768428D7061600018FA3 /* App.swift */; }; 6EA7768E28D7061600018FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6EA7768D28D7061600018FA3 /* Assets.xcassets */; }; 6EA7769228D7061600018FA3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6EA7769128D7061600018FA3 /* Preview Assets.xcassets */; }; @@ -190,6 +191,7 @@ 6E79A4B828DA63C400B44855 /* StackNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackNavigationView.swift; sourceTree = ""; }; 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyDetailView.swift; sourceTree = ""; }; 6E9E37AC28DADF2600BE7128 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; + 6E9E37AE28DAEBE000BE7128 /* ActivityItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityItem.swift; sourceTree = ""; }; 6EA7768128D7061600018FA3 /* SmartLock.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SmartLock.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6EA7768428D7061600018FA3 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 6EA7768D28D7061600018FA3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -370,6 +372,7 @@ 6E21830228D7C47500A622B3 /* Appearance.swift */, 6ED248CB28D7A3BF00F78D07 /* ActivityIndicatorView.swift */, 6E9E37AC28DADF2600BE7128 /* ActivityView.swift */, + 6E9E37AE28DAEBE000BE7128 /* ActivityItem.swift */, ); path = UIKit; sourceTree = ""; @@ -650,6 +653,7 @@ 6E21833E28D8F3B500A622B3 /* CreateNewKeyEventManagedObject.swift in Sources */, 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */, 6E21830728D7D08300A622B3 /* Permission.swift in Sources */, + 6E9E37AF28DAEBE000BE7128 /* ActivityItem.swift in Sources */, 6E21833F28D8F3B500A622B3 /* LockInformationManagedObject.swift in Sources */, 6E21833D28D8F3B500A622B3 /* EventManagedObject.swift in Sources */, 6E169A7828D9A34F008545EC /* NavigationLink.swift in Sources */, From 0a9aab692d73bb8859726b06e072f0c33cb3e7d4 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 21 Sep 2022 00:24:24 -0700 Subject: [PATCH 114/229] [CoreLock] Added `NewKey.Invitation.id` --- Sources/CoreLock/NewKey.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/CoreLock/NewKey.swift b/Sources/CoreLock/NewKey.swift index 4bce9ce8..50c4f610 100644 --- a/Sources/CoreLock/NewKey.swift +++ b/Sources/CoreLock/NewKey.swift @@ -61,3 +61,10 @@ public extension NewKey { } } } + +extension NewKey.Invitation: Identifiable { + + public var id: String { + return lock.description + "-" + key.id.description + } +} From 2f29c95ffc22be6171b396c43326b4b520448ddc Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 21 Sep 2022 00:36:46 -0700 Subject: [PATCH 115/229] [App] Fixed iOS sharing popover --- Xcode/LockKit/View/PermissionsView.swift | 28 ++++++++++-------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index 4b47b284..b4ee696c 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -82,6 +82,16 @@ public struct PermissionsView: View { Button(action: newPermission, label: { Image(systemSymbol: .plus) }) + #if os(iOS) + .popover(item: $newKeyInvitation, content: { key in + ActivityView( + activityItems: newKeyInvitation + .flatMap { [NewKeyFileActivityItem(invitation: $0)] as [Any] } ?? [], + applicationActivities: nil, + excludedActivityTypes: NewKeyFileActivityItem.excludedActivityTypes + ) + }) + #endif } } } @@ -112,22 +122,6 @@ public struct PermissionsView: View { } #endif } - #if os(iOS) - .sheet(isPresented: Binding(get: { - newKeyInvitation != nil - }, set: { - if $0 == false { - newKeyInvitation = nil - } - }), content: { - ActivityView( - activityItems: newKeyInvitation - .flatMap { [NewKeyFileActivityItem(invitation: $0)] as [Any] } ?? [], - applicationActivities: nil, - excludedActivityTypes: NewKeyFileActivityItem.excludedActivityTypes - ) - }) - #endif } public init(id: UUID) { @@ -178,7 +172,7 @@ private extension PermissionsView { func didCreateNewKey(_ newKey: NewKey.Invitation) { showNewKeyModal = false Task { - try? await Task.sleep(timeInterval: 1) + try? await Task.sleep(timeInterval: 0.6) self.newKeyInvitation = newKey } } From 58adc463789df768e7536258c832d8a059b1466a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 21 Sep 2022 01:48:31 -0700 Subject: [PATCH 116/229] [App] Added `TaskQueue` --- Xcode/LockKit/Extensions/Task.swift | 119 ++++++++++++++++++- Xcode/LockKit/Model/Store.swift | 4 +- Xcode/LockKit/View/EventsView.swift | 2 +- Xcode/LockKit/View/LockDetailView.swift | 4 +- Xcode/LockKit/View/NewPermissionView.swift | 3 +- Xcode/LockKit/View/PermissionsView.swift | 4 +- Xcode/SmartLock.xcodeproj/project.pbxproj | 2 - Xcode/SmartLock/View/NearbyDevicesView.swift | 8 +- 8 files changed, 133 insertions(+), 13 deletions(-) diff --git a/Xcode/LockKit/Extensions/Task.swift b/Xcode/LockKit/Extensions/Task.swift index edbfe203..13e2ffc8 100644 --- a/Xcode/LockKit/Extensions/Task.swift +++ b/Xcode/LockKit/Extensions/Task.swift @@ -5,9 +5,126 @@ // Created by Alsey Coleman Miller on 9/19/22. // -internal extension Task where Success == Never, Failure == Never { +public extension Task where Success == Never, Failure == Never { static func sleep(timeInterval: Double) async throws { try await sleep(nanoseconds: UInt64(timeInterval * Double(1_000_000_000))) } } + +// MARK: - Supporting Types + +public actor TaskQueue { + + // MARK: - Properties + + public let name: String + + public let priority: TaskPriority + + private var tasks = [PendingTask]() + + private var isRunning = false + + private var count: UInt64 = 0 + + // MARK: - Initialization + + public init( + name: String = "Task Queue " + UUID().description, + priority: TaskPriority = .userInitiated + ) { + self.name = name + self.priority = priority + } + + // MARK: - Methods + + public nonisolated func queue(after delay: Double? = nil, _ task: @escaping () async -> ()) { + Task.detached(priority: priority) { + // sleep before running task + if let duration = delay { + try? await Task.sleep(timeInterval: duration) + } + // execute and block global actor + await self.execute(task) + } + } + + private func pop() -> PendingTask? { + guard let pendingTask = tasks.first else { + return nil + } + let _ = tasks.removeFirst() + #if DEBUG + print("\(name) Will execute task \(pendingTask.id)") + print("\(name) \(tasks.map { $0.id })") + #endif + return pendingTask + } + + private func push(_ task: @escaping (() async -> ())) { + let pendingTask = PendingTask( + id: count, + work: task + ) + count += 1 + tasks.append(pendingTask) + #if DEBUG + print("\(name) Queued task \(pendingTask.id)") + print("\(name) \(tasks.map { $0.id })") + #endif + } + + private func lock(_ isRunning: Bool = true) { + self.isRunning = isRunning + } + + private func execute(_ task: @escaping () async -> ()) async { + // push task to stack + push(task) + // check lock to see if currently running + guard isRunning == false else { + return // the other detached task will run them all + } + lock() + // run in sequential order + while let queuedTask = pop() { + // execute task + await queuedTask.work() + #if DEBUG + print("\(name) Executed task \(queuedTask.id) of \(count - 1)") + print("\(name) \(tasks.map { $0.id })") + #endif + } + lock(false) // unlock + } +} + +private extension TaskQueue { + + struct PendingTask: Identifiable { + + let id: UInt64 + + let work: () async -> () + } +} + +// MARK: - Defined Task Queue + +public extension Task where Success == Never, Failure == Never { + + /// Serial task queue for Bluetooth. + static func bluetooth(_ task: @escaping () async -> ()) { + TaskQueue.bluetooth.queue(task) + } +} + +public extension TaskQueue { + + static let bluetooth = TaskQueue( + name: "com.colemancda.Lock.TaskQueue.Bluetooth", + priority: .userInitiated + ) +} diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 8660738d..c63a05e1 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -298,7 +298,7 @@ public extension Store { ) self.scanStream = stream // process scanned devices - Task { + Task.bluetooth { do { for try await scanData in stream { guard let serviceUUIDs = scanData.advertisementData.serviceUUIDs, @@ -311,7 +311,7 @@ public extension Store { } catch { log("⚠️ Unable to scan. \(error)") } - isScanning = false + self.isScanning = false } // stop scanning after 5 sec if need to read device info Task { diff --git a/Xcode/LockKit/View/EventsView.swift b/Xcode/LockKit/View/EventsView.swift index 52ea7394..7cebc3e6 100644 --- a/Xcode/LockKit/View/EventsView.swift +++ b/Xcode/LockKit/View/EventsView.swift @@ -91,7 +91,7 @@ private extension EventsView { func reload() { let locks = locks.sorted(by: { $0.description < $1.description }) - Task { + Task.bluetooth { let context = store.backgroundContext store.stopScanning() for lock in locks { diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 8c6d505a..5d5fe2c7 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -107,7 +107,7 @@ private extension LockDetailView { func reload() { let lock = self.id - Task { + Task.bluetooth { let context = store.backgroundContext store.stopScanning() // scan and find device @@ -137,7 +137,7 @@ private extension LockDetailView { let _ = try await store.listEvents(for: peripheral, fetchRequest: fetchRequest) } } catch { - log("⚠️ Error loading information for \(lock)") + log("⚠️ Error loading information for \(lock). \(error)") } } } diff --git a/Xcode/LockKit/View/NewPermissionView.swift b/Xcode/LockKit/View/NewPermissionView.swift index 67a49a8d..208ae4e9 100644 --- a/Xcode/LockKit/View/NewPermissionView.swift +++ b/Xcode/LockKit/View/NewPermissionView.swift @@ -65,7 +65,7 @@ private extension NewPermissionView { } } #else - Task { + Task.bluetooth { do { guard await store.central.state == .poweredOn else { throw LockError.bluetoothUnavailable @@ -85,6 +85,7 @@ private extension NewPermissionView { completion(newKey) } catch { state = .error(error.localizedDescription) + log("⚠️ Error creating new key for \(id). \(error)") } } #endif diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index b4ee696c..7f73f3fd 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -140,7 +140,7 @@ private extension PermissionsView { } func reload() { - Task { + Task.bluetooth { guard await store.central.state == .poweredOn else { return } @@ -153,6 +153,8 @@ private extension PermissionsView { return } try await store.listKeys(for: peripheral) + } catch { + log("⚠️ Error loading keys for \(id). \(error)") } } } diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index eb00e54f..9ba089bb 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 6E169A7828D9A34F008545EC /* NavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7728D9A34F008545EC /* NavigationLink.swift */; }; - 6E169A7928D9AA32008545EC /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E21834528D8FC4300A622B3 /* Task.swift */; }; 6E169A7D28D9C097008545EC /* PermissionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7C28D9C097008545EC /* PermissionsView.swift */; }; 6E169A7F28D9C135008545EC /* NewPermissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7E28D9C135008545EC /* NewPermissionView.swift */; }; 6E169A8128D9C15B008545EC /* PermissionScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A8028D9C15B008545EC /* PermissionScheduleView.swift */; }; @@ -617,7 +616,6 @@ 6E2182F128D7B4FA00A622B3 /* SettingsView.swift in Sources */, 6E4CB62528D792BF00116573 /* InfoPlist.swift in Sources */, 6E79A4BA28DA63FC00B44855 /* StackNavigationView.swift in Sources */, - 6E169A7928D9AA32008545EC /* Task.swift in Sources */, 6E21831D28D834D200A622B3 /* ContentView.swift in Sources */, 6EA7768528D7061600018FA3 /* App.swift in Sources */, 6E2182EF28D7B37000A622B3 /* TabBarView.swift in Sources */, diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index 2e4f9533..a866051a 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -28,7 +28,7 @@ struct NearbyDevicesView: View { ) .onAppear { if store.isScanning == false { - Task { + Task.bluetooth { try? await Task.sleep(timeInterval: 1.5) await scan() } @@ -48,7 +48,7 @@ private extension NearbyDevicesView { if store.isScanning { store.stopScanning() } else { - Task { + Task.bluetooth { await scan() } } @@ -59,7 +59,9 @@ private extension NearbyDevicesView { store.isScanning == false else { return } - await store.scan() + Task.bluetooth { + await store.scan() + } } var peripherals: [NativePeripheral] { From e9b636d8e1385d17e3108f271e1eaa8711e190fc Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 21 Sep 2022 02:18:36 -0700 Subject: [PATCH 117/229] [App] Added activity indicator to views --- Xcode/LockKit/Model/Store.swift | 24 +++++++++++--------- Xcode/LockKit/View/EventsView.swift | 19 ++++++++++++++-- Xcode/LockKit/View/LockDetailView.swift | 15 ++++++++++++ Xcode/LockKit/View/PermissionsView.swift | 13 +++++++++++ Xcode/SmartLock/View/NearbyDevicesView.swift | 6 +++-- 5 files changed, 62 insertions(+), 15 deletions(-) diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index c63a05e1..20ae3110 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -298,14 +298,14 @@ public extension Store { ) self.scanStream = stream // process scanned devices - Task.bluetooth { + Task { do { for try await scanData in stream { guard let serviceUUIDs = scanData.advertisementData.serviceUUIDs, serviceUUIDs.contains(LockService.uuid) else { continue } // cache found device - try? await Task.sleep(timeInterval: 0.5) + try? await Task.sleep(timeInterval: 0.6) self.peripherals[scanData.peripheral] = scanData } } catch { @@ -326,15 +326,17 @@ public extension Store { } // stop scanning and load info for unknown devices stopScanning() - for peripheral in loading() { - do { - let information = try await self.readInformation(for: peripheral) - log("Read information for lock \(information.id)") - #if DEBUG - dump(information) - #endif - } catch { - log("⚠️ Unable to load information for peripheral \(peripheral). \(error)") + Task.bluetooth { + for peripheral in loading() { + do { + let information = try await self.readInformation(for: peripheral) + log("Read information for lock \(information.id)") + #if DEBUG + dump(information) + #endif + } catch { + log("⚠️ Unable to load information for peripheral \(peripheral). \(error)") + } } } } diff --git a/Xcode/LockKit/View/EventsView.swift b/Xcode/LockKit/View/EventsView.swift index 7cebc3e6..2120c11f 100644 --- a/Xcode/LockKit/View/EventsView.swift +++ b/Xcode/LockKit/View/EventsView.swift @@ -45,7 +45,7 @@ public struct EventsView: View { }() @State - private var needsKeys = Set() + private var activityIndicator = false public var body: some View { list @@ -53,6 +53,17 @@ public struct EventsView: View { .onAppear { self._events.wrappedValue.nsPredicate = self.predicate } + #if os(iOS) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + if activityIndicator { + ProgressView() + .progressViewStyle(.circular) + } + } + } + #endif + } public init(lock: UUID? = nil) { @@ -92,8 +103,12 @@ private extension EventsView { func reload() { let locks = locks.sorted(by: { $0.description < $1.description }) Task.bluetooth { + activityIndicator = true + defer { Task { await MainActor.run { activityIndicator = false } } } let context = store.backgroundContext - store.stopScanning() + if store.isScanning { + store.stopScanning() + } for lock in locks { // load via Bonjour /* diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 5d5fe2c7..2ba0e574 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -19,6 +19,9 @@ public struct LockDetailView: View { public let id: UUID + @State + private var activityIndicator = false + public var body: some View { if let cache = self.cache { AnyView( @@ -36,6 +39,16 @@ public struct LockDetailView: View { .onAppear { reload() } + #if os(iOS) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + if activityIndicator { + ProgressView() + .progressViewStyle(.circular) + } + } + } + #endif ) } else if let information = self.information, information.status == .setup { @@ -108,6 +121,8 @@ private extension LockDetailView { func reload() { let lock = self.id Task.bluetooth { + activityIndicator = true + defer { Task { await MainActor.run { activityIndicator = false } } } let context = store.backgroundContext store.stopScanning() // scan and find device diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index 7f73f3fd..2e884c73 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -60,6 +60,9 @@ public struct PermissionsView: View { return dateFormatter }() + @State + private var activityIndicator = false + @State private var showNewKeyModal = false @@ -94,6 +97,14 @@ public struct PermissionsView: View { #endif } } + #if os(iOS) + ToolbarItem(placement: .navigationBarTrailing) { + if activityIndicator { + ProgressView() + .progressViewStyle(.circular) + } + } + #endif } .sheet(isPresented: $showNewKeyModal, onDismiss: { }) { #if os(iOS) @@ -144,6 +155,8 @@ private extension PermissionsView { guard await store.central.state == .poweredOn else { return } + activityIndicator = true + defer { Task { await MainActor.run { activityIndicator = false } } } do { if store.isScanning { store.stopScanning() diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index a866051a..ec71e3bf 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -28,9 +28,11 @@ struct NearbyDevicesView: View { ) .onAppear { if store.isScanning == false { - Task.bluetooth { + Task { try? await Task.sleep(timeInterval: 1.5) - await scan() + Task.bluetooth { + await scan() + } } } } From c08bb6e9ff7b52f601b9befa7c372745869bddbc Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 21 Sep 2022 02:38:20 -0700 Subject: [PATCH 118/229] [App] Reuse connection for loading information --- Sources/CoreLock/Bluetooth/Central.swift | 3 + Xcode/LockKit/Model/Store.swift | 97 ++++++++++++++---------- Xcode/LockKit/View/EventsView.swift | 40 +++++----- Xcode/LockKit/View/LockDetailView.swift | 44 ++++++----- 4 files changed, 106 insertions(+), 78 deletions(-) diff --git a/Sources/CoreLock/Bluetooth/Central.swift b/Sources/CoreLock/Bluetooth/Central.swift index 2bc68c05..33caabe4 100644 --- a/Sources/CoreLock/Bluetooth/Central.swift +++ b/Sources/CoreLock/Bluetooth/Central.swift @@ -28,6 +28,7 @@ public extension CentralManager { let cache = GATTConnection( central: self, + peripheral: peripheral, maximumTransmissionUnit: maximumTransmissionUnit, characteristics: characteristics ) @@ -49,6 +50,8 @@ public struct GATTConnection { internal unowned let central: Central + public let peripheral: Central.Peripheral + public let maximumTransmissionUnit: GATT.MaximumTransmissionUnit internal let characteristics: [BluetoothUUID: [Characteristic]] diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 20ae3110..e832bf8a 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -415,11 +415,17 @@ public extension Store { if isScanning { stopScanning() } - let information = try await central.readInformation( - for: peripheral - ) + let information = try await central.connection(for: peripheral) { + try await self.readInformation(for: $0) + } + return information + } + + @discardableResult + func readInformation(for connection: GATTConnection) async throws -> LockInformation { + let information = try await connection.readInformation() // update lock information cache - self.lockInformation[peripheral] = information + self.lockInformation[connection.peripheral] = information self[lock: information.id]?.information = LockCache.Information(information) log("Read information for \(information.id)") return information @@ -563,7 +569,15 @@ public extension Store { func listKeys( for peripheral: NativeCentral.Peripheral ) async throws { - + try await central.connection(for: peripheral) { + try await self.listKeys(for: $0) + } + } + + func listKeys( + for connection: GATTConnection + ) async throws { + let peripheral = connection.peripheral // get lock key guard let information = self.lockInformation[peripheral] else { throw LockError.unknownLock(peripheral) @@ -583,23 +597,22 @@ public extension Store { var newKeysCount = 0 // BLE request let centralLog = central.log - try await central.connection(for: peripheral) { - let stream = try await $0.listKeys(using: key, log: centralLog) - for try await notification in stream { - switch notification.key { - case let .key(key): - keysCount += 1 - centralLog?("Recieved \(key.permission.type) key \(key.id) \(key.name)") - case let .newKey(key): - newKeysCount += 1 - centralLog?("Recieved \(key.permission.type) key \(key.id) \(key.name)") - } - // call completion block - await context.commit { (context) in - try context.insert(notification.key, for: information.id) - } + let stream = try await connection.listKeys(using: key, log: centralLog) + for try await notification in stream { + switch notification.key { + case let .key(key): + keysCount += 1 + centralLog?("Recieved \(key.permission.type) key \(key.id) \(key.name)") + case let .newKey(key): + newKeysCount += 1 + centralLog?("Recieved \(key.permission.type) key \(key.id) \(key.name)") + } + // call completion block + await context.commit { (context) in + try context.insert(notification.key, for: information.id) } } + objectWillChange.send() // upload keys to cloud @@ -614,7 +627,16 @@ public extension Store { for peripheral: NativeCentral.Peripheral, fetchRequest: LockEvent.FetchRequest? = nil ) async throws { - + try await central.connection(for: peripheral) { + try await self.listEvents(for: $0, fetchRequest: fetchRequest) + } + } + + func listEvents( + for connection: GATTConnection, + fetchRequest: LockEvent.FetchRequest? = nil + ) async throws { + let peripheral = connection.peripheral // get lock key guard let information = self.lockInformation[peripheral] else { throw LockError.unknownLock(peripheral) @@ -633,27 +655,26 @@ public extension Store { var eventsCount = 0 // BLE request let centralLog = central.log - try await central.connection(for: peripheral) { - let stream = try await $0.listEvents(fetchRequest: fetchRequest, using: key, log: centralLog) - for try await notification in stream { - if let event = notification.event { - centralLog?("Recieved \(event.type) event \(event.id)") - eventsCount += 1 - // store in CoreData - await context.commit { (context) in - try context.insert(event, for: information.id) - } - // upload to iCloud - if preferences.isCloudBackupEnabled { - // perform concurrently - Task { - let value = LockEvent.Cloud(event: event, for: lockIdentifier) - try await self.cloud.upload(value) - } + let stream = try await connection.listEvents(fetchRequest: fetchRequest, using: key, log: centralLog) + for try await notification in stream { + if let event = notification.event { + centralLog?("Recieved \(event.type) event \(event.id)") + eventsCount += 1 + // store in CoreData + await context.commit { (context) in + try context.insert(event, for: information.id) + } + // upload to iCloud + if preferences.isCloudBackupEnabled { + // perform concurrently + Task { + let value = LockEvent.Cloud(event: event, for: lockIdentifier) + try await self.cloud.upload(value) } } } } + objectWillChange.send() log("Recieved \(eventsCount) events for lock \(information.id)") diff --git a/Xcode/LockKit/View/EventsView.swift b/Xcode/LockKit/View/EventsView.swift index 2120c11f..7bcc8ced 100644 --- a/Xcode/LockKit/View/EventsView.swift +++ b/Xcode/LockKit/View/EventsView.swift @@ -120,26 +120,28 @@ private extension EventsView { // scan and find device do { if let peripheral = try await store.device(for: lock) { - // load keys if admin - if let permssion = store[lock: lock]?.key.permission, permssion.isAdministrator { - try await store.listKeys(for: peripheral) - } - // load latest events - var lastEventDate: Date? - try? await context.perform { - lastEventDate = try context.find(id: lock, type: LockManagedObject.self) - .flatMap { try $0.lastEvent(in: context)?.date } - } - let fetchRequest = LockEvent.FetchRequest( - offset: 0, - limit: nil, - predicate: LockEvent.Predicate( - keys: nil, - start: lastEventDate, - end: nil + try await store.central.connection(for: peripheral) { connection in + // load keys if admin + if let permssion = store[lock: lock]?.key.permission, permssion.isAdministrator { + try await store.listKeys(for: connection) + } + // load latest events + var lastEventDate: Date? + try? await context.perform { + lastEventDate = try context.find(id: lock, type: LockManagedObject.self) + .flatMap { try $0.lastEvent(in: context)?.date } + } + let fetchRequest = LockEvent.FetchRequest( + offset: 0, + limit: nil, + predicate: LockEvent.Predicate( + keys: nil, + start: lastEventDate, + end: nil + ) ) - ) - try await store.listEvents(for: peripheral, fetchRequest: fetchRequest) + try await store.listEvents(for: connection, fetchRequest: fetchRequest) + } } } catch { log("⚠️ Error loading events for \(lock)") diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 2ba0e574..e983b92e 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -128,28 +128,30 @@ private extension LockDetailView { // scan and find device do { if let peripheral = try await store.device(for: lock) { - // read information - let _ = try await store.readInformation(for: peripheral) - // load keys if admin - if let permssion = store[lock: lock]?.key.permission, permssion.isAdministrator { - try await store.listKeys(for: peripheral) - } - // load latest events - var lastEventDate: Date? - try? await context.perform { - lastEventDate = try context.find(id: lock, type: LockManagedObject.self) - .flatMap { try $0.lastEvent(in: context)?.date } - } - let fetchRequest = LockEvent.FetchRequest( - offset: 0, - limit: nil, - predicate: LockEvent.Predicate( - keys: nil, - start: lastEventDate, - end: nil + try await store.central.connection(for: peripheral) { connection in + // read information + let _ = try await store.readInformation(for: connection) + // load keys if admin + if let permssion = store[lock: lock]?.key.permission, permssion.isAdministrator { + try await store.listKeys(for: connection) + } + // load latest events + var lastEventDate: Date? + try? await context.perform { + lastEventDate = try context.find(id: lock, type: LockManagedObject.self) + .flatMap { try $0.lastEvent(in: context)?.date } + } + let fetchRequest = LockEvent.FetchRequest( + offset: 0, + limit: nil, + predicate: LockEvent.Predicate( + keys: nil, + start: lastEventDate, + end: nil + ) ) - ) - let _ = try await store.listEvents(for: peripheral, fetchRequest: fetchRequest) + let _ = try await store.listEvents(for: connection, fetchRequest: fetchRequest) + } } } catch { log("⚠️ Error loading information for \(lock). \(error)") From 5c40bb64ac9b64175edba5306b96a46ad982359b Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 21 Sep 2022 02:47:16 -0700 Subject: [PATCH 119/229] [App] Fixed activity indicator not showing --- Xcode/LockKit/View/EventsView.swift | 4 ++++ Xcode/LockKit/View/LockDetailView.swift | 4 ++++ Xcode/LockKit/View/PermissionsView.swift | 5 +++-- Xcode/SmartLock/View/NearbyDevicesView.swift | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Xcode/LockKit/View/EventsView.swift b/Xcode/LockKit/View/EventsView.swift index 7bcc8ced..0f04bfa8 100644 --- a/Xcode/LockKit/View/EventsView.swift +++ b/Xcode/LockKit/View/EventsView.swift @@ -102,9 +102,13 @@ private extension EventsView { func reload() { let locks = locks.sorted(by: { $0.description < $1.description }) + activityIndicator = true Task.bluetooth { activityIndicator = true defer { Task { await MainActor.run { activityIndicator = false } } } + guard await store.central.state == .poweredOn else { + return + } let context = store.backgroundContext if store.isScanning { store.stopScanning() diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index e983b92e..0d02b645 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -120,9 +120,13 @@ private extension LockDetailView { func reload() { let lock = self.id + activityIndicator = true Task.bluetooth { activityIndicator = true defer { Task { await MainActor.run { activityIndicator = false } } } + guard await store.central.state == .poweredOn else { + return + } let context = store.backgroundContext store.stopScanning() // scan and find device diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index 2e884c73..0183d965 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -151,12 +151,13 @@ private extension PermissionsView { } func reload() { + activityIndicator = true Task.bluetooth { + activityIndicator = true + defer { Task { await MainActor.run { activityIndicator = false } } } guard await store.central.state == .poweredOn else { return } - activityIndicator = true - defer { Task { await MainActor.run { activityIndicator = false } } } do { if store.isScanning { store.stopScanning() diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index ec71e3bf..24681542 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -29,7 +29,7 @@ struct NearbyDevicesView: View { .onAppear { if store.isScanning == false { Task { - try? await Task.sleep(timeInterval: 1.5) + try? await Task.sleep(timeInterval: 0.7) Task.bluetooth { await scan() } From a0e3dba148041e3f38fc21934767a8eff7d4339a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 21 Sep 2022 03:11:20 -0700 Subject: [PATCH 120/229] [App] Added `UIImage.permissionType()` --- Xcode/LockKit/View/UIKit/ActivityItem.swift | 14 +++++++++++--- .../View/UIKit/PermissionIconViewUIView.swift | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Xcode/LockKit/View/UIKit/ActivityItem.swift b/Xcode/LockKit/View/UIKit/ActivityItem.swift index 9e0aa38d..4e7fcfc1 100644 --- a/Xcode/LockKit/View/UIKit/ActivityItem.swift +++ b/Xcode/LockKit/View/UIKit/ActivityItem.swift @@ -48,13 +48,21 @@ public final class NewKeyFileActivityItem: UIActivityItemProvider { // MARK: - UIActivityItemSource - public override func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String { + public override func activityViewController( + _ activityViewController: UIActivityViewController, + subjectForActivityType activityType: UIActivity.ActivityType? + ) -> String { return invitation.key.name } - public override func activityViewController(_ activityViewController: UIActivityViewController, thumbnailImageForActivityType activityType: UIActivity.ActivityType?, suggestedSize size: CGSize) -> UIImage? { - return nil//return UIImage(permissionType: invitation.key.permission.type) + public override func activityViewController( + _ activityViewController: UIActivityViewController, + thumbnailImageForActivityType activityType: UIActivity.ActivityType?, + suggestedSize size: CGSize + ) -> UIImage? { + + return UIImage.permissionType(invitation.key.permission.type, size: size) } /* @available(iOSApplicationExtension 13.0, *) diff --git a/Xcode/LockKit/View/UIKit/PermissionIconViewUIView.swift b/Xcode/LockKit/View/UIKit/PermissionIconViewUIView.swift index 147df8f6..722c3e90 100644 --- a/Xcode/LockKit/View/UIKit/PermissionIconViewUIView.swift +++ b/Xcode/LockKit/View/UIKit/PermissionIconViewUIView.swift @@ -83,6 +83,20 @@ public extension PermissionIconView.UIViewType { } } +// MARK: - Image Rendering + +public extension UIImage { + + static func permissionType(_ permissionType: PermissionType, size: CGSize = CGSize(width: 32, height: 32)) -> UIImage { + let view = PermissionIconView.UIViewType(permission: permissionType, frame: CGRect(origin: .zero, size: size)) + let renderer = UIGraphicsImageRenderer(size: view.bounds.size) + let image = renderer.image { context in + view.drawHierarchy(in: view.bounds, afterScreenUpdates: true) + } + return image + } +} + // MARK: - Supporting Types private extension PermissionIconView.UIViewType { From 61c938bd6ac19da2dda37ce6739b98d0df85122d Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 21 Sep 2022 03:11:52 -0700 Subject: [PATCH 121/229] [App] Reload keys after sharing --- Xcode/LockKit/View/PermissionsView.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index 0183d965..430e1caa 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -186,11 +186,16 @@ private extension PermissionsView { } func didCreateNewKey(_ newKey: NewKey.Invitation) { + // reload pending keys + reload() + // hide modal showNewKeyModal = false + // show popover Task { try? await Task.sleep(timeInterval: 0.6) self.newKeyInvitation = newKey } + } } From ed294623bd4be49facacd4c27d43bbf069524372 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 21 Sep 2022 05:34:58 -0700 Subject: [PATCH 122/229] [App] Fixed scanning --- Xcode/LockKit/Extensions/Task.swift | 112 ++++++++++++---- Xcode/LockKit/Model/Store.swift | 26 ++-- Xcode/LockKit/Model/iCloud/iCloud.swift | 9 +- Xcode/LockKit/View/EventsView.swift | 92 ++++++------- Xcode/LockKit/View/LockDetailView.swift | 128 +++++++++++-------- Xcode/LockKit/View/NewPermissionView.swift | 42 +++--- Xcode/LockKit/View/PermissionsView.swift | 47 ++++--- Xcode/SmartLock/View/NearbyDevicesView.swift | 36 +++--- Xcode/SmartLock/View/SidebarView.swift | 2 +- Xcode/SmartLock/View/TabBarView.swift | 2 +- 10 files changed, 304 insertions(+), 192 deletions(-) diff --git a/Xcode/LockKit/Extensions/Task.swift b/Xcode/LockKit/Extensions/Task.swift index 13e2ffc8..4c218c0d 100644 --- a/Xcode/LockKit/Extensions/Task.swift +++ b/Xcode/LockKit/Extensions/Task.swift @@ -24,6 +24,8 @@ public actor TaskQueue { private var tasks = [PendingTask]() + private var currentTask: (id: UInt64, task: Task)? + private var isRunning = false private var count: UInt64 = 0 @@ -40,15 +42,34 @@ public actor TaskQueue { // MARK: - Methods - public nonisolated func queue(after delay: Double? = nil, _ task: @escaping () async -> ()) { - Task.detached(priority: priority) { + public func queue(after delay: Double? = nil, _ task: @escaping () async -> ()) async -> PendingTask { + return await Task(priority: priority) { // sleep before running task if let duration = delay { try? await Task.sleep(timeInterval: duration) } // execute and block global actor - await self.execute(task) + return await self.execute(task) + }.value + } + + public func cancelAll() { + var count = tasks.count + if currentTask != nil { + count += 1 + } + //currentTask?.task.cancel() + currentTask = nil + tasks.removeAll(keepingCapacity: true) + isRunning = false + tasks.forEach { task in + Task { await task.setFinished() } + } + #if DEBUG + if count > 0 { + NSLog("\(name) Cancelled \(count) tasks") } + #endif } private func pop() -> PendingTask? { @@ -57,57 +78,103 @@ public actor TaskQueue { } let _ = tasks.removeFirst() #if DEBUG - print("\(name) Will execute task \(pendingTask.id)") - print("\(name) \(tasks.map { $0.id })") + NSLog("\(name) Will execute task \(pendingTask.id)") #endif return pendingTask } - private func push(_ task: @escaping (() async -> ())) { + private func push(_ task: @escaping (() async -> ())) -> PendingTask { + let id = self.count + let name = self.name let pendingTask = PendingTask( - id: count, - work: task + id: id, + work: task, + onTermination: { + Task { + // cancel current executing task + if let task = self.currentTask, task.id == id { + self.currentTask = nil + //task.task.cancel() + #if DEBUG + NSLog("\(name) Cancelled task \(id)") + #endif + } + } + } ) count += 1 tasks.append(pendingTask) #if DEBUG - print("\(name) Queued task \(pendingTask.id)") - print("\(name) \(tasks.map { $0.id })") + NSLog("\(name) Queued task \(pendingTask.id)") #endif + return pendingTask } private func lock(_ isRunning: Bool = true) { self.isRunning = isRunning } - private func execute(_ task: @escaping () async -> ()) async { + private func execute(_ task: @escaping () async -> ()) async -> PendingTask { // push task to stack - push(task) + let newTask = push(task) // check lock to see if currently running guard isRunning == false else { - return // the other detached task will run them all + return newTask // the other detached task will run them all } lock() // run in sequential order while let queuedTask = pop() { + // skip cancelled task + guard await queuedTask.didFinish == false else { + NSLog("\(name) Skipped task \(queuedTask.id)") + continue + } // execute task - await queuedTask.work() + let task = Task(priority: priority) { + await queuedTask.work() + } + currentTask = (queuedTask.id, task) + await task.value // wait for task to finish + currentTask = nil + await queuedTask.setFinished() #if DEBUG - print("\(name) Executed task \(queuedTask.id) of \(count - 1)") - print("\(name) \(tasks.map { $0.id })") + NSLog("\(name) Executed task \(queuedTask.id) of \(count - 1)") #endif } lock(false) // unlock + return newTask } } -private extension TaskQueue { +public extension TaskQueue { - struct PendingTask: Identifiable { + actor PendingTask: Identifiable { + + public let id: UInt64 + + internal let work: () async -> () + + internal let onTermination: () -> () + + internal fileprivate(set) var didFinish = false - let id: UInt64 + fileprivate init(id: UInt64, work: @escaping () async -> (), onTermination: @escaping () -> Void) { + self.id = id + self.work = work + self.onTermination = onTermination + } - let work: () async -> () + public func cancel() { + guard didFinish == false else { + return + } + setFinished() + onTermination() + } + + fileprivate func setFinished() { + didFinish = true + } } } @@ -116,8 +183,9 @@ private extension TaskQueue { public extension Task where Success == Never, Failure == Never { /// Serial task queue for Bluetooth. - static func bluetooth(_ task: @escaping () async -> ()) { - TaskQueue.bluetooth.queue(task) + @discardableResult + static func bluetooth(_ task: @escaping () async -> ()) async -> TaskQueue.PendingTask { + return await TaskQueue.bluetooth.queue(task) } } diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index e832bf8a..272f35ad 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -180,7 +180,7 @@ public extension Store { await updateCoreData() // update CloudKit do { try await syncCloud() } - catch { log("⚠️ Unable to upload locks to iCloud") } + catch { log("⚠️ Unable to upload locks to iCloud. \(error)") } } var applicationData: ApplicationData { @@ -270,7 +270,7 @@ public extension Store { await self.scan() } } - try await Task.sleep(timeInterval: 1) + try await Task.sleep(timeInterval: 0.5) } } } @@ -292,6 +292,8 @@ public extension Store { self.scanStream = nil let filterDuplicates = true //preferences.filterDuplicates self.peripherals.removeAll(keepingCapacity: true) + stopScanning() + isScanning = true let stream = central.scan( with: [LockService.uuid], filterDuplicates: filterDuplicates @@ -326,8 +328,9 @@ public extension Store { } // stop scanning and load info for unknown devices stopScanning() - Task.bluetooth { + await Task.bluetooth { for peripheral in loading() { + self.stopScanning() do { let information = try await self.readInformation(for: peripheral) log("Read information for lock \(information.id)") @@ -354,6 +357,7 @@ public extension Store { for id: UUID, scanDuration duration: TimeInterval = 2.0 ) async throws -> NativeCentral.Peripheral? { + stopScanning() if let peripheral = self[peripheral: id] { return peripheral } else { @@ -362,9 +366,11 @@ public extension Store { with: [LockService.uuid], filterDuplicates: filterDuplicates ) + self.scanStream = stream + self.isScanning = true Task { try? await Task.sleep(timeInterval: duration) - stream.stop() + stopScanning() } for try await scanData in stream { guard let serviceUUIDs = scanData.advertisementData.serviceUUIDs, @@ -375,10 +381,11 @@ public extension Store { // if found and information has cached, stop scanning if let information = lockInformation[peripheral], information.id == id { - stream.stop() + stopScanning() return peripheral // return first found device } } + self.isScanning = false // scan stopped due to timeout for peripheral in peripherals.keys { // skip known locks that are not the targeted device @@ -412,9 +419,7 @@ public extension Store { throw LockError.bluetoothUnavailable } // stop scanning - if isScanning { - stopScanning() - } + stopScanning() let information = try await central.connection(for: peripheral) { try await self.readInformation(for: $0) } @@ -437,6 +442,7 @@ public extension Store { using sharedSecret: KeyData, name: String ) async throws { + stopScanning() let setupRequest = SetupRequest() let information = try await central.setup( setupRequest, @@ -569,6 +575,7 @@ public extension Store { func listKeys( for peripheral: NativeCentral.Peripheral ) async throws { + stopScanning() try await central.connection(for: peripheral) { try await self.listKeys(for: $0) } @@ -611,6 +618,8 @@ public extension Store { await context.commit { (context) in try context.insert(notification.key, for: information.id) } + // remove old keys + // FIXME: Remove old keys } objectWillChange.send() @@ -627,6 +636,7 @@ public extension Store { for peripheral: NativeCentral.Peripheral, fetchRequest: LockEvent.FetchRequest? = nil ) async throws { + stopScanning() try await central.connection(for: peripheral) { try await self.listEvents(for: $0, fetchRequest: fetchRequest) } diff --git a/Xcode/LockKit/Model/iCloud/iCloud.swift b/Xcode/LockKit/Model/iCloud/iCloud.swift index 8a471e00..2c3c4547 100644 --- a/Xcode/LockKit/Model/iCloud/iCloud.swift +++ b/Xcode/LockKit/Model/iCloud/iCloud.swift @@ -297,7 +297,7 @@ internal extension ApplicationData { public extension Store { #if os(iOS) - func cloudDidChangeExternally() { + func cloudDidChangeExternally() async { if let lastUpdatedCloud = self.cloud.lastUpdated() { guard self.applicationData.updated != lastUpdatedCloud @@ -305,13 +305,10 @@ public extension Store { } log("☁️ iCloud changed externally") - /* do { - try self.syncCloud(conflicts: { _ in - return nil - }) + await try self.syncCloud() } - catch { log("⚠️ Could not sync iCloud: \(error.localizedDescription)") }*/ + catch { log("⚠️ Could not sync iCloud: \(error.localizedDescription)") } } #endif diff --git a/Xcode/LockKit/View/EventsView.swift b/Xcode/LockKit/View/EventsView.swift index 0f04bfa8..24d8b832 100644 --- a/Xcode/LockKit/View/EventsView.swift +++ b/Xcode/LockKit/View/EventsView.swift @@ -47,6 +47,9 @@ public struct EventsView: View { @State private var activityIndicator = false + @State + private var reloadTask: TaskQueue.PendingTask? + public var body: some View { list .navigationTitle("History") @@ -103,52 +106,55 @@ private extension EventsView { func reload() { let locks = locks.sorted(by: { $0.description < $1.description }) activityIndicator = true - Task.bluetooth { - activityIndicator = true - defer { Task { await MainActor.run { activityIndicator = false } } } - guard await store.central.state == .poweredOn else { - return - } - let context = store.backgroundContext - if store.isScanning { - store.stopScanning() - } - for lock in locks { - // load via Bonjour - /* - do { - - } catch { - log("⚠️ Error loading events for \(lock) via Bonjour") - }*/ - // scan and find device - do { - if let peripheral = try await store.device(for: lock) { - try await store.central.connection(for: peripheral) { connection in - // load keys if admin - if let permssion = store[lock: lock]?.key.permission, permssion.isAdministrator { - try await store.listKeys(for: connection) - } - // load latest events - var lastEventDate: Date? - try? await context.perform { - lastEventDate = try context.find(id: lock, type: LockManagedObject.self) - .flatMap { try $0.lastEvent(in: context)?.date } - } - let fetchRequest = LockEvent.FetchRequest( - offset: 0, - limit: nil, - predicate: LockEvent.Predicate( - keys: nil, - start: lastEventDate, - end: nil + Task { + await self.reloadTask?.cancel() + self.reloadTask = await Task.bluetooth { + activityIndicator = true + defer { Task { await MainActor.run { activityIndicator = false } } } + guard await store.central.state == .poweredOn else { + return + } + let context = store.backgroundContext + if store.isScanning { + store.stopScanning() + } + for lock in locks { + // load via Bonjour + /* + do { + + } catch { + log("⚠️ Error loading events for \(lock) via Bonjour") + }*/ + // scan and find device + do { + if let peripheral = try await store.device(for: lock) { + try await store.central.connection(for: peripheral) { connection in + // load keys if admin + if let permssion = store[lock: lock]?.key.permission, permssion.isAdministrator { + try await store.listKeys(for: connection) + } + // load latest events + var lastEventDate: Date? + try? await context.perform { + lastEventDate = try context.find(id: lock, type: LockManagedObject.self) + .flatMap { try $0.lastEvent(in: context)?.date } + } + let fetchRequest = LockEvent.FetchRequest( + offset: 0, + limit: nil, + predicate: LockEvent.Predicate( + keys: nil, + start: lastEventDate, + end: nil + ) ) - ) - try await store.listEvents(for: connection, fetchRequest: fetchRequest) + try await store.listEvents(for: connection, fetchRequest: fetchRequest) + } } + } catch { + log("⚠️ Error loading events for \(lock). \(error)") } - } catch { - log("⚠️ Error loading events for \(lock)") } } } diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 0d02b645..efcfec7d 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -22,6 +22,9 @@ public struct LockDetailView: View { @State private var activityIndicator = false + @State + private var pendingTask: TaskQueue.PendingTask? + public var body: some View { if let cache = self.cache { AnyView( @@ -73,18 +76,38 @@ private extension LockDetailView { store.lockInformation.first(where: { $0.value.id == id })?.value } - func unlock() async { - let authentication = LAContext() - let policy: LAPolicy = .deviceOwnerAuthenticationWithBiometrics - let reason = NSLocalizedString("Biometrics are needed to unlock", comment: "") - do { - if try authentication.canEvaluate(policy: policy) { - try await authentication.evaluatePolicy(policy, localizedReason: reason) + func unlock() { + // FIXME: Handler errors + Task { + guard await store.central.state == .poweredOn else { + return + } + // FaceID + let authentication = LAContext() + let policy: LAPolicy = .deviceOwnerAuthenticationWithBiometrics + let reason = NSLocalizedString("Biometrics are needed to unlock", comment: "") + do { + if try authentication.canEvaluate(policy: policy) { + try await authentication.evaluatePolicy(policy, localizedReason: reason) + } + } + catch { + log("⚠️ Unable to authenticate for unlock \(id). \(error)") + } + // cancel all operation + if store.isScanning { + store.stopScanning() + } + await TaskQueue.bluetooth.cancelAll() + await pendingTask?.cancel() + pendingTask = await Task.bluetooth { + do { + // Bluetooth request + try await store.unlock(for: id, action: .default) + } catch { + log("⚠️ Unable to unlock \(id). \(error)") + } } - try await store.unlock(for: id, action: .default) - } - catch { - log("⚠️ Unable to unlock \(id). \(error)") } } @@ -121,44 +144,47 @@ private extension LockDetailView { func reload() { let lock = self.id activityIndicator = true - Task.bluetooth { - activityIndicator = true - defer { Task { await MainActor.run { activityIndicator = false } } } - guard await store.central.state == .poweredOn else { - return - } - let context = store.backgroundContext - store.stopScanning() - // scan and find device - do { - if let peripheral = try await store.device(for: lock) { - try await store.central.connection(for: peripheral) { connection in - // read information - let _ = try await store.readInformation(for: connection) - // load keys if admin - if let permssion = store[lock: lock]?.key.permission, permssion.isAdministrator { - try await store.listKeys(for: connection) - } - // load latest events - var lastEventDate: Date? - try? await context.perform { - lastEventDate = try context.find(id: lock, type: LockManagedObject.self) - .flatMap { try $0.lastEvent(in: context)?.date } - } - let fetchRequest = LockEvent.FetchRequest( - offset: 0, - limit: nil, - predicate: LockEvent.Predicate( - keys: nil, - start: lastEventDate, - end: nil + Task { + await pendingTask?.cancel() + await pendingTask = Task.bluetooth { + activityIndicator = true + defer { Task { await MainActor.run { activityIndicator = false } } } + guard await store.central.state == .poweredOn else { + return + } + let context = store.backgroundContext + store.stopScanning() + // scan and find device + do { + if let peripheral = try await store.device(for: lock) { + try await store.central.connection(for: peripheral) { connection in + // read information + let _ = try await store.readInformation(for: connection) + // load latest events + var lastEventDate: Date? + try? await context.perform { + lastEventDate = try context.find(id: lock, type: LockManagedObject.self) + .flatMap { try $0.lastEvent(in: context)?.date } + } + let fetchRequest = LockEvent.FetchRequest( + offset: 0, + limit: nil, + predicate: LockEvent.Predicate( + keys: nil, + start: lastEventDate, + end: nil + ) ) - ) - let _ = try await store.listEvents(for: connection, fetchRequest: fetchRequest) + let _ = try await store.listEvents(for: connection, fetchRequest: fetchRequest) + // load keys if admin + if let permssion = store[lock: lock]?.key.permission, permssion.isAdministrator { + try await store.listKeys(for: connection) + } + } } + } catch { + log("⚠️ Error loading information for \(lock). \(error)") } - } catch { - log("⚠️ Error loading information for \(lock). \(error)") } } } @@ -181,7 +207,7 @@ extension LockDetailView { @State var showID = false - let unlock: () async -> () + let unlock: () -> () @State private var enableActions = true @@ -203,13 +229,7 @@ extension LockDetailView { HStack { Spacer() // unlock button - Button(action: { - //enableActions = false - Task { - await unlock() - //enableActions = true - } - }, label: { + Button(action: unlock, label: { PermissionIconView(permission: cache.key.permission.type) .frame(width: 150, height: 150, alignment: .center) }) diff --git a/Xcode/LockKit/View/NewPermissionView.swift b/Xcode/LockKit/View/NewPermissionView.swift index 208ae4e9..49ef6386 100644 --- a/Xcode/LockKit/View/NewPermissionView.swift +++ b/Xcode/LockKit/View/NewPermissionView.swift @@ -65,27 +65,29 @@ private extension NewPermissionView { } } #else - Task.bluetooth { - do { - guard await store.central.state == .poweredOn else { - throw LockError.bluetoothUnavailable - } - if store.isScanning { - store.stopScanning() - } - guard let peripheral = try await store.device(for: id) else { - throw LockError.notInRange(lock: id) + Task { + await Task.bluetooth { + do { + guard await store.central.state == .poweredOn else { + throw LockError.bluetoothUnavailable + } + if store.isScanning { + store.stopScanning() + } + guard let peripheral = try await store.device(for: id) else { + throw LockError.notInRange(lock: id) + } + let newKey = try await store.newKey( + for: peripheral, + permission: permission, + name: name + ) + state = .editing + completion(newKey) + } catch { + state = .error(error.localizedDescription) + log("⚠️ Error creating new key for \(id). \(error)") } - let newKey = try await store.newKey( - for: peripheral, - permission: permission, - name: name - ) - state = .editing - completion(newKey) - } catch { - state = .error(error.localizedDescription) - log("⚠️ Error creating new key for \(id). \(error)") } } #endif diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index 430e1caa..03a8a9ae 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -63,6 +63,9 @@ public struct PermissionsView: View { @State private var activityIndicator = false + @State + private var reloadTask: TaskQueue.PendingTask? + @State private var showNewKeyModal = false @@ -152,24 +155,28 @@ private extension PermissionsView { func reload() { activityIndicator = true - Task.bluetooth { - activityIndicator = true - defer { Task { await MainActor.run { activityIndicator = false } } } - guard await store.central.state == .poweredOn else { - return - } - do { - if store.isScanning { - store.stopScanning() - } - guard let peripheral = try await store.device(for: id) else { - // unable to find device + Task { + await reloadTask?.cancel() + reloadTask = await Task.bluetooth { + activityIndicator = true + defer { Task { await MainActor.run { activityIndicator = false } } } + guard await store.central.state == .poweredOn else { return } - try await store.listKeys(for: peripheral) - } catch { - log("⚠️ Error loading keys for \(id). \(error)") + do { + if store.isScanning { + store.stopScanning() + } + guard let peripheral = try await store.device(for: id) else { + // unable to find device + return + } + try await store.listKeys(for: peripheral) + } catch { + log("⚠️ Error loading keys for \(id). \(error)") + } } + } } @@ -186,16 +193,18 @@ private extension PermissionsView { } func didCreateNewKey(_ newKey: NewKey.Invitation) { - // reload pending keys - reload() // hide modal showNewKeyModal = false // show popover Task { - try? await Task.sleep(timeInterval: 0.6) + try? await Task.sleep(timeInterval: 0.2) self.newKeyInvitation = newKey } - + Task { + try? await Task.sleep(timeInterval: 1.0) + // reload pending keys + reload() + } } } diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index 24681542..e055db1f 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -13,6 +13,9 @@ struct NearbyDevicesView: View { @EnvironmentObject var store: Store + @SwiftUI.State + private var scanTask: TaskQueue.PendingTask? + var body: some View { StateView( state: state, @@ -27,12 +30,11 @@ struct NearbyDevicesView: View { } ) .onAppear { - if store.isScanning == false { - Task { - try? await Task.sleep(timeInterval: 0.7) - Task.bluetooth { - await scan() - } + Task { + // start scanning after delay + try? await Task.sleep(timeInterval: 0.7) + if store.isScanning == false { + toggleScan() } } } @@ -50,22 +52,20 @@ private extension NearbyDevicesView { if store.isScanning { store.stopScanning() } else { - Task.bluetooth { - await scan() + Task { + await scanTask?.cancel() + await TaskQueue.bluetooth.cancelAll() // stop all pending operations to scan + scanTask = await Task.bluetooth { + guard await store.central.state == .poweredOn, + store.isScanning == false else { + return + } + await store.scan() + } } } } - func scan() async { - guard await store.central.state == .poweredOn, - store.isScanning == false else { - return - } - Task.bluetooth { - await store.scan() - } - } - var peripherals: [NativePeripheral] { store.peripherals .lazy diff --git a/Xcode/SmartLock/View/SidebarView.swift b/Xcode/SmartLock/View/SidebarView.swift index 7e90df5d..41270fa4 100644 --- a/Xcode/SmartLock/View/SidebarView.swift +++ b/Xcode/SmartLock/View/SidebarView.swift @@ -75,7 +75,7 @@ struct SidebarView: View { } Task { do { try await Store.shared.syncCloud(conflicts: { _ in return true }) } // always override on macOS - catch { log("⚠️ Unable to automatically sync with iCloud") } + catch { log("⚠️ Unable to automatically sync with iCloud. \(error)") } } } } diff --git a/Xcode/SmartLock/View/TabBarView.swift b/Xcode/SmartLock/View/TabBarView.swift index 1c549339..98ab2b3d 100644 --- a/Xcode/SmartLock/View/TabBarView.swift +++ b/Xcode/SmartLock/View/TabBarView.swift @@ -52,7 +52,7 @@ struct TabBarView: View { .onAppear { Task { do { try await Store.shared.syncCloud() } - catch { log("⚠️ Unable to automatically sync with iCloud") } + catch { log("⚠️ Unable to automatically sync with iCloud. \(error)") } } } } From b06b841afdba2a34d20de26ba6f3246105542ab3 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 21 Sep 2022 06:13:31 -0700 Subject: [PATCH 123/229] [App] Remove stale keys --- Xcode/LockKit/Model/Store.swift | 63 ++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 272f35ad..c491ebfb 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -600,26 +600,71 @@ public extension Store { ) let context = backgroundContext - var keysCount = 0 - var newKeysCount = 0 + var keys = Set() + var newKeys = Set() // BLE request let centralLog = central.log let stream = try await connection.listKeys(using: key, log: centralLog) for try await notification in stream { switch notification.key { case let .key(key): - keysCount += 1 + keys.insert(key.id) centralLog?("Recieved \(key.permission.type) key \(key.id) \(key.name)") case let .newKey(key): - newKeysCount += 1 - centralLog?("Recieved \(key.permission.type) key \(key.id) \(key.name)") + newKeys.insert(key.id) + centralLog?("Recieved \(key.permission.type) pending key \(key.id) \(key.name)") } - // call completion block + // insert key to CoreData await context.commit { (context) in try context.insert(notification.key, for: information.id) } - // remove old keys - // FIXME: Remove old keys + } + // remove other keys from CoreData + Task { + await context.commit { (context) in + do { + let fetchRequest = KeyManagedObject.fetchRequest() + fetchRequest.predicate = NSPredicate( + format: "%K == %@ && NOT %@ CONTAINS %K", + #keyPath(NewKeyManagedObject.lock.identifier), + lockIdentifier as NSUUID, + keys as NSSet, + #keyPath(KeyManagedObject.identifier) + ) + // fetch + let invalidKeys = try context.fetch(fetchRequest) + // remove keys from CoreData + invalidKeys.forEach { + context.delete($0) + } + if invalidKeys.isEmpty == false { + log("Removed \(invalidKeys.count) invalid keys from cache") + } + } + + do { + let fetchRequest = NewKeyManagedObject.fetchRequest() + if newKeys.isEmpty == false { + fetchRequest.predicate = NSPredicate( + format: "%K == %@ && NOT %@ CONTAINS %K", + #keyPath(NewKeyManagedObject.lock.identifier), + lockIdentifier as NSUUID, + newKeys as NSSet, + #keyPath(NewKeyManagedObject.identifier) + ) + } + // fetch + let invalidKeys = try context.fetch(fetchRequest) + print("Invalid keys ", invalidKeys.count) + // remove keys from CoreData + invalidKeys.forEach { + context.delete($0) + } + if invalidKeys.isEmpty == false { + log("Removed \(invalidKeys.count) invalid pending keys from cache") + } + } + } } objectWillChange.send() @@ -629,7 +674,7 @@ public extension Store { //updateCloud() } - log("Recieved \(keysCount) keys and \(newKeysCount) pending keys for lock \(information.id)") + log("Recieved \(keys.count) keys and \(newKeys.count) pending keys for lock \(information.id)") } func listEvents( From ed2e9530a19c2f0fd79ae0eeb2ffb967841aa935 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 21 Sep 2022 12:44:51 -0700 Subject: [PATCH 124/229] [App] Added `Predicate` dependency --- .../ConfirmNewKeyEventManagedObject.swift | 6 +- .../Model/CoreData/ContactManagedObject.swift | 1 + .../CreateNewKeyEventManagedObject.swift | 5 +- .../Model/CoreData/EventManagedObject.swift | 2 + .../Model/CoreData/ManagedObject.swift | 13 +++- Xcode/LockKit/Model/Store.swift | 59 +++++++++++++++---- Xcode/LockKit/Model/iCloud/CloudShare.swift | 9 ++- Xcode/LockKit/View/PermissionsView.swift | 4 +- Xcode/SmartLock.xcodeproj/project.pbxproj | 17 ++++++ .../xcshareddata/swiftpm/Package.resolved | 9 +++ 10 files changed, 104 insertions(+), 21 deletions(-) diff --git a/Xcode/LockKit/Model/CoreData/ConfirmNewKeyEventManagedObject.swift b/Xcode/LockKit/Model/CoreData/ConfirmNewKeyEventManagedObject.swift index 4f97d530..525a41dd 100644 --- a/Xcode/LockKit/Model/CoreData/ConfirmNewKeyEventManagedObject.swift +++ b/Xcode/LockKit/Model/CoreData/ConfirmNewKeyEventManagedObject.swift @@ -9,6 +9,7 @@ import Foundation import CoreData import CoreLock +import Predicate public final class ConfirmNewKeyEventManagedObject: EventManagedObject { @@ -69,7 +70,10 @@ public extension ConfirmNewKeyEventManagedObject { let fetchRequest = NSFetchRequest() fetchRequest.entity = CreateNewKeyEventManagedObject.entity() - fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(CreateNewKeyEventManagedObject.pendingKey), newKey as NSUUID) + let predicate = (.keyPath(#keyPath(CreateNewKeyEventManagedObject.pendingKey)) == .value(.uuid(newKey))).toFoundation() + fetchRequest.predicate = predicate + assert(predicate.description == NSPredicate(format: "%K == %@", #keyPath(CreateNewKeyEventManagedObject.pendingKey), newKey as NSUUID).description) + assert(predicate == NSPredicate(format: "%K == %@", #keyPath(CreateNewKeyEventManagedObject.pendingKey), newKey as NSUUID)) fetchRequest.fetchLimit = 1 fetchRequest.includesSubentities = false fetchRequest.returnsObjectsAsFaults = false diff --git a/Xcode/LockKit/Model/CoreData/ContactManagedObject.swift b/Xcode/LockKit/Model/CoreData/ContactManagedObject.swift index f5c75064..7dfe73f6 100644 --- a/Xcode/LockKit/Model/CoreData/ContactManagedObject.swift +++ b/Xcode/LockKit/Model/CoreData/ContactManagedObject.swift @@ -11,6 +11,7 @@ import CoreData import CoreLock import CloudKit import Contacts +import Predicate public final class ContactManagedObject: NSManagedObject { diff --git a/Xcode/LockKit/Model/CoreData/CreateNewKeyEventManagedObject.swift b/Xcode/LockKit/Model/CoreData/CreateNewKeyEventManagedObject.swift index 428d352f..52fe730d 100644 --- a/Xcode/LockKit/Model/CoreData/CreateNewKeyEventManagedObject.swift +++ b/Xcode/LockKit/Model/CoreData/CreateNewKeyEventManagedObject.swift @@ -9,6 +9,7 @@ import Foundation import CoreData import CoreLock +import Predicate public final class CreateNewKeyEventManagedObject: EventManagedObject { @@ -66,7 +67,9 @@ public extension CreateNewKeyEventManagedObject { let fetchRequest = NSFetchRequest() fetchRequest.entity = ConfirmNewKeyEventManagedObject.entity() - fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(ConfirmNewKeyEventManagedObject.pendingKey), newKey as NSUUID) + let predicate = (.keyPath(#keyPath(ConfirmNewKeyEventManagedObject.pendingKey)) == .value(.uuid(newKey))).toFoundation() + assert(predicate == NSPredicate(format: "%K == %@", #keyPath(ConfirmNewKeyEventManagedObject.pendingKey), newKey as NSUUID)) + fetchRequest.predicate = predicate fetchRequest.fetchLimit = 1 fetchRequest.includesSubentities = false fetchRequest.returnsObjectsAsFaults = false diff --git a/Xcode/LockKit/Model/CoreData/EventManagedObject.swift b/Xcode/LockKit/Model/CoreData/EventManagedObject.swift index bab923e1..cf739d36 100644 --- a/Xcode/LockKit/Model/CoreData/EventManagedObject.swift +++ b/Xcode/LockKit/Model/CoreData/EventManagedObject.swift @@ -9,6 +9,7 @@ import Foundation import CoreData import CoreLock +import Predicate public class EventManagedObject: NSManagedObject { @@ -103,6 +104,7 @@ public extension LockManagedObject { ascending: false ) ] + //let predicate = (.keyPath(#keyPath(EventManagedObject.lock.identifier)) == .value(self.identifier!)) fetchRequest.predicate = NSPredicate( format: "%K == %@", #keyPath(EventManagedObject.lock), diff --git a/Xcode/LockKit/Model/CoreData/ManagedObject.swift b/Xcode/LockKit/Model/CoreData/ManagedObject.swift index 44517645..26e64a1e 100644 --- a/Xcode/LockKit/Model/CoreData/ManagedObject.swift +++ b/Xcode/LockKit/Model/CoreData/ManagedObject.swift @@ -8,6 +8,7 @@ import Foundation import CoreData +import Predicate public extension NSManagedObjectModel { @@ -71,7 +72,13 @@ internal extension NSManagedObjectContext { let fetchRequest = NSFetchRequest() fetchRequest.entity = T.entity() - fetchRequest.predicate = NSPredicate(format: "%K == %@", propertyName, identifier) + if let uuid = identifier as? NSUUID { + let predicate = (.keyPath(.init(rawValue: propertyName)) == .value(.uuid(uuid as UUID))).toFoundation() + fetchRequest.predicate = predicate + assert(predicate == NSPredicate(format: "%K == %@", propertyName, identifier)) + } else { + fetchRequest.predicate = NSPredicate(format: "%K == %@", propertyName, identifier) + } fetchRequest.fetchLimit = 1 fetchRequest.includesSubentities = true fetchRequest.returnsObjectsAsFaults = false @@ -90,7 +97,9 @@ public extension NSManagedObjectContext { let fetchRequest = NSFetchRequest() fetchRequest.entity = T.entity() - fetchRequest.predicate = NSPredicate(format: "%K == %@", "identifier", id as NSUUID) + let predicate = ("identifier" == id).toFoundation() + fetchRequest.predicate = predicate + assert(predicate == NSPredicate(format: "%K == %@", "identifier", id as NSUUID)) fetchRequest.fetchLimit = 1 fetchRequest.includesSubentities = true fetchRequest.returnsObjectsAsFaults = false diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index c491ebfb..c7cf6ad2 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -13,6 +13,7 @@ import Combine @_exported import CoreLock import DarwinGATT import KeychainAccess +import Predicate /// Lock Store object @MainActor @@ -619,18 +620,34 @@ public extension Store { try context.insert(notification.key, for: information.id) } } + // remove other keys from CoreData Task { await context.commit { (context) in do { let fetchRequest = KeyManagedObject.fetchRequest() - fetchRequest.predicate = NSPredicate( + let predicate = ( + (#keyPath(KeyManagedObject.lock.identifier) == lockIdentifier) + && .compound(.not( + .comparison( + Comparison( + left: .value(.collection(keys.map { .uuid($0) })), + right: .keyPath(#keyPath(KeyManagedObject.identifier)), + type: .contains + ) + ) + )) + ) + fetchRequest.predicate = predicate.toFoundation() + assert(NSPredicate( format: "%K == %@ && NOT %@ CONTAINS %K", - #keyPath(NewKeyManagedObject.lock.identifier), + #keyPath(KeyManagedObject.lock.identifier), lockIdentifier as NSUUID, keys as NSSet, #keyPath(KeyManagedObject.identifier) - ) + ).description == predicate.description) + assert(predicate.description == predicate.toFoundation().description) + print(predicate.toFoundation().description) // fetch let invalidKeys = try context.fetch(fetchRequest) // remove keys from CoreData @@ -644,18 +661,28 @@ public extension Store { do { let fetchRequest = NewKeyManagedObject.fetchRequest() - if newKeys.isEmpty == false { - fetchRequest.predicate = NSPredicate( - format: "%K == %@ && NOT %@ CONTAINS %K", - #keyPath(NewKeyManagedObject.lock.identifier), - lockIdentifier as NSUUID, - newKeys as NSSet, - #keyPath(NewKeyManagedObject.identifier) - ) - } + let predicate = ( + (#keyPath(NewKeyManagedObject.lock.identifier) == lockIdentifier) + && .compound(.not( + .comparison( + Comparison( + left: .value(.collection(newKeys.map { .uuid($0) })), + right: .keyPath(#keyPath(NewKeyManagedObject.identifier)), + type: .contains + ) + ) + )) + ) + fetchRequest.predicate = predicate.toFoundation() + assert(NSPredicate( + format: "%K == %@ && NOT %@ CONTAINS %K", + #keyPath(NewKeyManagedObject.lock.identifier), + lockIdentifier as NSUUID, + newKeys as NSSet, + #keyPath(NewKeyManagedObject.identifier) + ).description == predicate.description) // fetch let invalidKeys = try context.fetch(fetchRequest) - print("Invalid keys ", invalidKeys.count) // remove keys from CoreData invalidKeys.forEach { context.delete($0) @@ -664,6 +691,12 @@ public extension Store { log("Removed \(invalidKeys.count) invalid pending keys from cache") } } + + Task { + await MainActor.run { [weak self] in + self?.objectWillChange.send() + } + } } } diff --git a/Xcode/LockKit/Model/iCloud/CloudShare.swift b/Xcode/LockKit/Model/iCloud/CloudShare.swift index 94374c70..96e962a3 100644 --- a/Xcode/LockKit/Model/iCloud/CloudShare.swift +++ b/Xcode/LockKit/Model/iCloud/CloudShare.swift @@ -10,6 +10,7 @@ import Foundation import CloudKit import CloudKitCodable import CoreLock +import Predicate #if os(iOS) import UIKit @@ -87,9 +88,11 @@ internal extension CloudStore { } else { userID = try await container.fetchUserRecordID() } + let predicate = (.keyPath("user") == .value(.string(userID.recordName))).toFoundation() + assert(predicate == NSPredicate(format: "%K == %@", "user", userID.recordName)) let query = CKQuery( recordType: CloudShare.NewKey.ID.cloudRecordType, - predicate: NSPredicate(format: "%K == %@", "user", userID.recordName) + predicate: predicate ) return database.queryAll(query) .map { try decoder.decode(CloudShare.NewKey.self, from: $0) } @@ -102,9 +105,11 @@ public extension CloudStore { func subcribeNewKeyShares() async throws { let user = try await container.fetchUserRecordID() + let predicate = (.keyPath("user") == .value(.string(user.recordName))).toFoundation() + assert(predicate == NSPredicate(format: "%K == %@", "user", user.recordName)) let subcription = CKQuerySubscription( recordType: CloudShare.NewKey.ID.cloudRecordType, - predicate: NSPredicate(format: "%K == %@", "user", user.recordName), + predicate: predicate, options: [.firesOnRecordCreation] ) let notificationInfo = CKQuerySubscription.NotificationInfo() diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index 03a8a9ae..bf2a593a 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -74,8 +74,8 @@ public struct PermissionsView: View { public var body: some View { StateView( - keys: keys.lazy.map { Key(managedObject: $0)! }, - newKeys: newKeys.lazy.map { NewKey(managedObject: $0)! }, + keys: keys.lazy.compactMap { Key(managedObject: $0) }, + newKeys: newKeys.lazy.compactMap { NewKey(managedObject: $0) }, reload: reload ) .onAppear { diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 9ba089bb..cb812c00 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -83,6 +83,7 @@ 6E9E37AB28DAA6D100BE7128 /* KeyDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */; }; 6E9E37AD28DADF2600BE7128 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AC28DADF2600BE7128 /* ActivityView.swift */; }; 6E9E37AF28DAEBE000BE7128 /* ActivityItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AE28DAEBE000BE7128 /* ActivityItem.swift */; }; + 6E9E37B228DB852A00BE7128 /* Predicate in Frameworks */ = {isa = PBXBuildFile; productRef = 6E9E37B128DB852A00BE7128 /* Predicate */; }; 6EA7768528D7061600018FA3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA7768428D7061600018FA3 /* App.swift */; }; 6EA7768E28D7061600018FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6EA7768D28D7061600018FA3 /* Assets.xcassets */; }; 6EA7769228D7061600018FA3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6EA7769128D7061600018FA3 /* Preview Assets.xcassets */; }; @@ -222,6 +223,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6E9E37B228DB852A00BE7128 /* Predicate in Frameworks */, 6E169A8428D9C25C008545EC /* SFSafeSymbols in Frameworks */, 6E21830A28D7FEA900A622B3 /* KeychainAccess in Frameworks */, 6E3276D928D70FA000AF171B /* GATT in Frameworks */, @@ -521,6 +523,7 @@ 6E21830928D7FEA900A622B3 /* KeychainAccess */, 6E21836928D953D000A622B3 /* CloudKitCodable */, 6E169A8328D9C25C008545EC /* SFSafeSymbols */, + 6E9E37B128DB852A00BE7128 /* Predicate */, ); productName = LockKit; productReference = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; @@ -562,6 +565,7 @@ 6E21830828D7FEA900A622B3 /* XCRemoteSwiftPackageReference "KeychainAccess" */, 6E21836828D953D000A622B3 /* XCRemoteSwiftPackageReference "CloudKitCodable" */, 6E169A8228D9C25C008545EC /* XCRemoteSwiftPackageReference "SFSafeSymbols" */, + 6E9E37B028DB852A00BE7128 /* XCRemoteSwiftPackageReference "Predicate" */, ); productRefGroup = 6EA7768228D7061600018FA3 /* Products */; projectDirPath = ""; @@ -1109,6 +1113,14 @@ kind = branch; }; }; + 6E9E37B028DB852A00BE7128 /* XCRemoteSwiftPackageReference "Predicate" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/PureSwift/Predicate"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1141,6 +1153,11 @@ package = 6E3276D528D70FA000AF171B /* XCRemoteSwiftPackageReference "GATT" */; productName = GATT; }; + 6E9E37B128DB852A00BE7128 /* Predicate */ = { + isa = XCSwiftPackageProductDependency; + package = 6E9E37B028DB852A00BE7128 /* XCRemoteSwiftPackageReference "Predicate" */; + productName = Predicate; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 66f531c4..60874f94 100644 --- a/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -45,6 +45,15 @@ "revision" : "ee878eaeb3efa09753a9e29e8deb8c331b96f3c8" } }, + { + "identity" : "predicate", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/Predicate", + "state" : { + "revision" : "7e0d2fd1fa4104db408903ea3d7def9bc613f8fb", + "version" : "1.1.1" + } + }, { "identity" : "sfsafesymbols", "kind" : "remoteSourceControl", From 22fc3f3354c4a09ac872c2fc590b5c84d0d53319 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 21 Sep 2022 13:33:40 -0700 Subject: [PATCH 125/229] [App] Removed `StackNavigationView` --- Xcode/SmartLock.xcodeproj/project.pbxproj | 4 -- .../SmartLock/View/StackNavigationView.swift | 72 ------------------- 2 files changed, 76 deletions(-) delete mode 100644 Xcode/SmartLock/View/StackNavigationView.swift diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index cb812c00..2214d2b8 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -79,7 +79,6 @@ 6E4CB62128D7907200116573 /* PermissionIconViewUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62028D7907200116573 /* PermissionIconViewUIView.swift */; }; 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62228D7907E00116573 /* PermissionIconViewNSView.swift */; }; 6E4CB62528D792BF00116573 /* InfoPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62428D792BF00116573 /* InfoPlist.swift */; }; - 6E79A4BA28DA63FC00B44855 /* StackNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E79A4B828DA63C400B44855 /* StackNavigationView.swift */; }; 6E9E37AB28DAA6D100BE7128 /* KeyDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */; }; 6E9E37AD28DADF2600BE7128 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AC28DADF2600BE7128 /* ActivityView.swift */; }; 6E9E37AF28DAEBE000BE7128 /* ActivityItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AE28DAEBE000BE7128 /* ActivityItem.swift */; }; @@ -188,7 +187,6 @@ 6E4CB62028D7907200116573 /* PermissionIconViewUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionIconViewUIView.swift; sourceTree = ""; }; 6E4CB62228D7907E00116573 /* PermissionIconViewNSView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionIconViewNSView.swift; sourceTree = ""; }; 6E4CB62428D792BF00116573 /* InfoPlist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InfoPlist.swift; path = ../../../iOS/SmartLock/InfoPlist.swift; sourceTree = ""; }; - 6E79A4B828DA63C400B44855 /* StackNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackNavigationView.swift; sourceTree = ""; }; 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyDetailView.swift; sourceTree = ""; }; 6E9E37AC28DADF2600BE7128 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; 6E9E37AE28DAEBE000BE7128 /* ActivityItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityItem.swift; sourceTree = ""; }; @@ -308,7 +306,6 @@ 6E3276CF28D70C9400AF171B /* NearbyDevicesView.swift */, 6E2182EE28D7B37000A622B3 /* TabBarView.swift */, 6E21831A28D8341000A622B3 /* SidebarView.swift */, - 6E79A4B828DA63C400B44855 /* StackNavigationView.swift */, 6E21831E28D84A7300A622B3 /* SidebarLabel.swift */, 6E21831C28D834D200A622B3 /* ContentView.swift */, 6E2182F028D7B4FA00A622B3 /* SettingsView.swift */, @@ -619,7 +616,6 @@ files = ( 6E2182F128D7B4FA00A622B3 /* SettingsView.swift in Sources */, 6E4CB62528D792BF00116573 /* InfoPlist.swift in Sources */, - 6E79A4BA28DA63FC00B44855 /* StackNavigationView.swift in Sources */, 6E21831D28D834D200A622B3 /* ContentView.swift in Sources */, 6EA7768528D7061600018FA3 /* App.swift in Sources */, 6E2182EF28D7B37000A622B3 /* TabBarView.swift in Sources */, diff --git a/Xcode/SmartLock/View/StackNavigationView.swift b/Xcode/SmartLock/View/StackNavigationView.swift deleted file mode 100644 index a2d48cd1..00000000 --- a/Xcode/SmartLock/View/StackNavigationView.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// StackNavigationView.swift -// LockKit -// -// Created by Alsey Coleman Miller on 9/20/22. -// - -#if os(macOS) -import SwiftUI - -/// Stack Navigation View for macOS -/// -/// https://betterprogramming.pub/stack-navigation-on-macos-41a40d8ec3a4 -struct StackNavigationView : View where RootContent: View { - - @Binding - var currentSubview: AnyView - - @Binding - var showingSubview: Bool - - let rootView: () -> RootContent - - var body: some View { - VStack { - if !showingSubview { - rootView() - } else { - StackNavigationSubview(isVisible: $showingSubview) { - currentSubview - } - .transition(.move(edge: .trailing)) - } - } - } - - init( - currentSubview: Binding, - showingSubview: Binding, - @ViewBuilder rootView: @escaping () -> RootContent) { - self._currentSubview = currentSubview - self._showingSubview = showingSubview - self.rootView = rootView - } -} - -private struct StackNavigationSubview: View where Content: View { - - @Binding - var isVisible: Bool - - let contentView: () -> Content - - var body: some View { - VStack { - contentView() // subview - } - .toolbar { - ToolbarItem(placement: .navigation) { - Button(action: { - withAnimation(.easeOut(duration: 0.3)) { - isVisible = false - } - }, label: { - Label("back", systemImage: "chevron.left") - }) - } - } - } -} - -#endif From de6f86d765d4bb69fecde09b7d2c841c4471e24c Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 21 Sep 2022 15:31:47 -0700 Subject: [PATCH 126/229] [App] Fixed macOS navigation --- Xcode/LockKit/View/NavigationLink.swift | 23 ++- Xcode/SmartLock/View/NearbyDevicesView.swift | 2 +- Xcode/SmartLock/View/SidebarView.swift | 178 ++++++++++--------- 3 files changed, 117 insertions(+), 86 deletions(-) diff --git a/Xcode/LockKit/View/NavigationLink.swift b/Xcode/LockKit/View/NavigationLink.swift index fcf6e8d3..78a261e4 100644 --- a/Xcode/LockKit/View/NavigationLink.swift +++ b/Xcode/LockKit/View/NavigationLink.swift @@ -7,8 +7,6 @@ import SwiftUI -public var AppNavigationLinkNavigate: (AppNavigationLink.ID, AnyView) -> () = { _, _ in assertionFailure() } - public struct AppNavigationLink : View { public typealias ID = AppNavigationLinkID @@ -19,6 +17,11 @@ public struct AppNavigationLink : View { private let label: Label + #if os(macOS) + @EnvironmentObject + private var coordinator: AppNavigationLinkCoordinator + #endif + public var body: some View { #if os(macOS) Button( @@ -38,12 +41,16 @@ public struct AppNavigationLink : View { } } +#if os(macOS) private extension AppNavigationLink { func buttonAction() { - AppNavigationLinkNavigate(id, AnyView(destination)) + coordinator.current = (id: id, view: AnyView(destination)) } } +#endif + +// MARK: - Supporting Types public enum AppNavigationLinkID: Hashable { @@ -84,3 +91,13 @@ public extension AppNavigationLinkID { } } } + +#if os(macOS) +public final class AppNavigationLinkCoordinator: ObservableObject { + + @Published + public var current: (id: AppNavigationLinkID, view: AnyView)? + + public init() { } +} +#endif diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index e055db1f..5c68bc95 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -160,7 +160,7 @@ private extension NearbyDevicesView.StateView { switch state { case .bluetoothUnavailable: Image(systemName: "exclamationmark.triangle.fill") - .symbolRenderingMode(.monochrome) + .symbolRenderingMode(.multicolor) case .scanning: Image(systemName: "stop.fill") .symbolRenderingMode(.monochrome) diff --git a/Xcode/SmartLock/View/SidebarView.swift b/Xcode/SmartLock/View/SidebarView.swift index 41270fa4..a01a1a73 100644 --- a/Xcode/SmartLock/View/SidebarView.swift +++ b/Xcode/SmartLock/View/SidebarView.swift @@ -23,66 +23,58 @@ struct SidebarView: View { @State private var isKeysExpanded = true - @State - private var navigationStack = [(id: AppNavigationLinkID, view: AnyView)]() + @StateObject + var coordinator = AppNavigationLinkCoordinator() var body: some View { - SwiftUI.NavigationView { - SidebarView.NavigationView( + NavigationView { + SidebarNavigationView( selection: Binding( get: { sidebarSelection }, set: { sidebarSelectionChanged($0) } ), - isScanning: store.isScanning, + scanStatus: scanStatus, locks: locks, keys: keys, isNearbyExpanded: Binding( get: { isNearbyExpanded }, set: { toggleNearbyExpanded($0) } ), - isKeysExpanded: $isKeysExpanded + isKeysExpanded: $isKeysExpanded, + toggleScan: toggleScan ) - switch navigationStack.count { - case 0: - Text("Select a lock") - case 1: + if navigationStack.count > 0 { navigationStack[0].view - case 2: - SwiftUI.NavigationView { - navigationStack[0].view - .frame(minWidth: 350) - navigationStack[1].view - .frame(minWidth: 350) - } - default: - SwiftUI.NavigationView { - navigationStack[0].view - .frame(minWidth: 350) - navigationStack[1].view - .frame(minWidth: 350) - navigationStack[2].view - .frame(minWidth: 350) - } - + } + if navigationStack.count > 1 { + navigationStack[1].view } } .navigationViewStyle(.columns) - .frame(minHeight: 550) + .frame(minWidth: 550, minHeight: 550) .onAppear { - // configure navigation links - AppNavigationLinkNavigate = { (id, view) in - navigate(id: id, view: view) - } Task { do { try await Store.shared.syncCloud(conflicts: { _ in return true }) } // always override on macOS catch { log("⚠️ Unable to automatically sync with iCloud. \(error)") } } } + .environmentObject(coordinator) + } } private extension SidebarView { + var scanStatus: ScanStatus { + if store.isScanning { + return .scanning + } else if store.state != .poweredOn { + return .bluetoothUnavailable + } else { + return .stopScan + } + } + var peripherals: [NativePeripheral] { store.peripherals.keys.sorted(by: { $0.id.description < $1.id.description }) } @@ -98,6 +90,24 @@ private extension SidebarView { .map { .key($0.key.id, $0.name, $0.key.permission.type) } } + func toggleScan() { + if store.isScanning { + store.stopScanning() + } else { + Task { + //await scanTask?.cancel() + await TaskQueue.bluetooth.cancelAll() // stop all pending operations to scan + await Task.bluetooth { + guard await store.central.state == .poweredOn, + store.isScanning == false else { + return + } + await store.scan() + } + } + } + } + func toggleNearbyExpanded(_ newValue: Bool) { isNearbyExpanded = newValue // start scanning if not already @@ -113,30 +123,19 @@ private extension SidebarView { } } - func navigate(id: AppNavigationLinkID, view: AnyView) { - //guard navigationStack.contains(where: { $0.id == id }) == false else { - // return - //} - - // special cases - switch id.type { - case .lock: - navigationStack = [(id, view)] - case .permissions, - .events: - navigationStack = (navigationStack.first.flatMap { [$0] } ?? []) + [(id, view)] - default: - break + var navigationStack: [(id: AppNavigationLinkID, view: AnyView)] { + guard let current = coordinator.current else { + return [] } - - // try to replace existing of same type - if let index = navigationStack.firstIndex(where: { $0.id.type == id.type }) { - navigationStack[index] = (id, view) - } else if navigationStack.count > 2 { - navigationStack[1] = navigationStack[2] // push stack, max 3 - navigationStack[2] = (id, view) - } else { - navigationStack.append((id, view)) // push stack + switch current.id { + case let .lock(lock): + return [current, (.events(lock), AnyView(EventsView(lock: lock)))] + case let .events(lock): + return [(.lock(lock), AnyView(LockDetailView(id: lock))), current] + case let .permissions(lock): + return [(.lock(lock), AnyView(LockDetailView(id: lock))), current] + default: + return [current] } } @@ -164,14 +163,14 @@ private extension SidebarView { return } let lock = information.id - navigationStack = [(.lock(lock), detailView(for: lock))] + coordinator.current = (.lock(lock), detailView(for: lock)) case let .key(keyID, _, _): guard let lock = store.applicationData.locks.first(where: { $0.value.key.id == keyID })?.key else { // invalid key selection assertionFailure("Selected unknown key \(keyID)") return } - navigationStack = [(.lock(lock), detailView(for: lock))] + coordinator.current = (.lock(lock), detailView(for: lock)) } } @@ -179,28 +178,6 @@ private extension SidebarView { return AnyView( LockDetailView(id: lock) ) - - /* - .toolbar { - ToolbarItem(placement: .navigation) { - Button(action: { - Task { - if store.isScanning { - store.stopScanning() - } else { - await store.scan() - } - } - }, label: { - if store.isScanning { - Label("stop", systemImage: "stop.fill") - } else { - Label("scan", systemImage: "arrow.clockwise") - } - }) - } - } - */ } func item(for peripheral: NativePeripheral) -> Item { @@ -258,12 +235,12 @@ private extension SidebarView { extension SidebarView { - struct NavigationView: View { + struct SidebarNavigationView: View { @Binding var selection: Item.ID? - let isScanning: Bool + let scanStatus: ScanStatus let locks: [Item] @@ -275,11 +252,13 @@ extension SidebarView { @Binding var isKeysExpanded: Bool + var toggleScan: () -> () + var body: some View { List(selection: $selection) { Group( title: "Nearby", - image: isScanning ? .loading : .symbol("antenna.radiowaves.left.and.right"), + image: scanStatus == .scanning ? .loading : .symbol("antenna.radiowaves.left.and.right"), items: locks, isExpanded: $isNearbyExpanded ) @@ -290,10 +269,45 @@ extension SidebarView { isExpanded: $isKeysExpanded ) } + .toolbar { + ToolbarItem(placement: .primaryAction) { + scanButton + } + } } } } +private extension SidebarView.SidebarNavigationView { + + var scanButton: some View { + Button(action: { + toggleScan() + }, label: { + switch scanStatus { + case .bluetoothUnavailable: + Image(systemName: "exclamationmark.triangle.fill") + .symbolRenderingMode(.multicolor) + case .scanning: + Image(systemName: "stop.fill") + .symbolRenderingMode(.monochrome) + case .stopScan: + Image(systemName: "arrow.clockwise") + .symbolRenderingMode(.monochrome) + } + }) + } +} + +extension SidebarView { + + enum ScanStatus { + case bluetoothUnavailable + case scanning + case stopScan + } +} + extension SidebarView { struct Group: View { From 146225f47a46d0309bda9a918e3903129e719888 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 21 Sep 2022 17:47:11 -0700 Subject: [PATCH 127/229] [App] Fixed macOS navigation stack --- Xcode/LockKit/View/NavigationLink.swift | 32 ------------------- Xcode/SmartLock.xcodeproj/project.pbxproj | 4 +-- Xcode/SmartLock/App.swift | 24 +++++++++++++- Xcode/SmartLock/View/SidebarView.swift | 39 ++++++----------------- 4 files changed, 34 insertions(+), 65 deletions(-) diff --git a/Xcode/LockKit/View/NavigationLink.swift b/Xcode/LockKit/View/NavigationLink.swift index 78a261e4..81163b9f 100644 --- a/Xcode/LockKit/View/NavigationLink.swift +++ b/Xcode/LockKit/View/NavigationLink.swift @@ -17,21 +17,8 @@ public struct AppNavigationLink : View { private let label: Label - #if os(macOS) - @EnvironmentObject - private var coordinator: AppNavigationLinkCoordinator - #endif - public var body: some View { - #if os(macOS) - Button( - action: buttonAction, - label: { label } - ) - .buttonStyle(.plain) - #else NavigationLink(destination: destination, label: { label }) - #endif } public init(id: ID, destination: () -> Destination, label: () -> Label) { @@ -41,15 +28,6 @@ public struct AppNavigationLink : View { } } -#if os(macOS) -private extension AppNavigationLink { - - func buttonAction() { - coordinator.current = (id: id, view: AnyView(destination)) - } -} -#endif - // MARK: - Supporting Types public enum AppNavigationLinkID: Hashable { @@ -91,13 +69,3 @@ public extension AppNavigationLinkID { } } } - -#if os(macOS) -public final class AppNavigationLinkCoordinator: ObservableObject { - - @Published - public var current: (id: AppNavigationLinkID, view: AnyView)? - - public init() { } -} -#endif diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 2214d2b8..cedb61a0 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -810,7 +810,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -866,7 +866,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/Xcode/SmartLock/App.swift b/Xcode/SmartLock/App.swift index 3219b44d..ab7b8613 100644 --- a/Xcode/SmartLock/App.swift +++ b/Xcode/SmartLock/App.swift @@ -8,9 +8,22 @@ import SwiftUI import LockKit +#if os(macOS) +import AppKit +#elseif canImport(UIKit) +import UIKit +#endif + @main struct LockApp: App { - + + #if os(macOS) + @NSApplicationDelegateAdaptor(AppDelegate.self) + var appDelegate + #elseif os(iOS) || os(tvOS) + + #endif + var body: some Scene { WindowGroup { ContentView() @@ -37,3 +50,12 @@ struct LockApp: App { #endif }() } + +#if os(macOS) +final class AppDelegate: NSResponder, NSApplicationDelegate { + + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return false + } +} +#endif diff --git a/Xcode/SmartLock/View/SidebarView.swift b/Xcode/SmartLock/View/SidebarView.swift index a01a1a73..4f7ad9f8 100644 --- a/Xcode/SmartLock/View/SidebarView.swift +++ b/Xcode/SmartLock/View/SidebarView.swift @@ -23,8 +23,8 @@ struct SidebarView: View { @State private var isKeysExpanded = true - @StateObject - var coordinator = AppNavigationLinkCoordinator() + @State + private var detail: AnyView = AnyView(Text("Select a lock")) var body: some View { NavigationView { @@ -43,11 +43,8 @@ struct SidebarView: View { isKeysExpanded: $isKeysExpanded, toggleScan: toggleScan ) - if navigationStack.count > 0 { - navigationStack[0].view - } - if navigationStack.count > 1 { - navigationStack[1].view + NavigationStack { + detail } } .navigationViewStyle(.columns) @@ -58,7 +55,6 @@ struct SidebarView: View { catch { log("⚠️ Unable to automatically sync with iCloud. \(error)") } } } - .environmentObject(coordinator) } } @@ -123,22 +119,6 @@ private extension SidebarView { } } - var navigationStack: [(id: AppNavigationLinkID, view: AnyView)] { - guard let current = coordinator.current else { - return [] - } - switch current.id { - case let .lock(lock): - return [current, (.events(lock), AnyView(EventsView(lock: lock)))] - case let .events(lock): - return [(.lock(lock), AnyView(LockDetailView(id: lock))), current] - case let .permissions(lock): - return [(.lock(lock), AnyView(LockDetailView(id: lock))), current] - default: - return [current] - } - } - func sidebarSelectionChanged(_ newValue: Item.ID?) { sidebarSelection = newValue // deselect @@ -163,21 +143,20 @@ private extension SidebarView { return } let lock = information.id - coordinator.current = (.lock(lock), detailView(for: lock)) + detail = AnyView(detailView(for: lock)) case let .key(keyID, _, _): guard let lock = store.applicationData.locks.first(where: { $0.value.key.id == keyID })?.key else { // invalid key selection assertionFailure("Selected unknown key \(keyID)") return } - coordinator.current = (.lock(lock), detailView(for: lock)) + + detail = AnyView(detailView(for: lock)) } } - func detailView(for lock: UUID) -> AnyView { - return AnyView( - LockDetailView(id: lock) - ) + func detailView(for lock: UUID) -> some View { + LockDetailView(id: lock) } func item(for peripheral: NativePeripheral) -> Item { From 92943baf3817c0251265cb576d2af98d299684e9 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 21 Sep 2022 21:07:51 -0700 Subject: [PATCH 128/229] [App] Updated `AppNavigationLink` --- Sources/CoreLock/EventStore.swift | 4 +- Sources/CoreLock/NewKey.swift | 2 +- .../LockKit/Model/NewKeyInvitationStore.swift | 17 ++++ Xcode/LockKit/Model/Predicate.swift | 23 +++++ Xcode/LockKit/Model/iCloud/iCloud.swift | 2 +- Xcode/LockKit/View/EventsView.swift | 61 +++++++------- Xcode/LockKit/View/KeyDetailView.swift | 4 +- Xcode/LockKit/View/LockDetailView.swift | 8 +- Xcode/LockKit/View/NavigationLink.swift | 74 ++++++++++++++--- Xcode/LockKit/View/NewKeyInvitationView.swift | 39 +++++++++ Xcode/LockKit/View/PermissionsView.swift | 8 +- Xcode/SmartLock.xcodeproj/project.pbxproj | 12 +++ Xcode/SmartLock/App.swift | 16 ++++ Xcode/SmartLock/View/SettingsView.swift | 11 ++- Xcode/SmartLock/View/SidebarLabel.swift | 9 +- Xcode/SmartLock/View/SidebarView.swift | 83 +++++++++++-------- Xcode/SmartLock/View/TabBarView.swift | 8 +- 17 files changed, 274 insertions(+), 107 deletions(-) create mode 100644 Xcode/LockKit/Model/NewKeyInvitationStore.swift create mode 100644 Xcode/LockKit/Model/Predicate.swift create mode 100644 Xcode/LockKit/View/NewKeyInvitationView.swift diff --git a/Sources/CoreLock/EventStore.swift b/Sources/CoreLock/EventStore.swift index 7073ede8..7c8b19d9 100644 --- a/Sources/CoreLock/EventStore.swift +++ b/Sources/CoreLock/EventStore.swift @@ -36,7 +36,7 @@ public final class InMemoryLockEvents: LockEventStore { public extension LockEvent { - struct FetchRequest: Codable, Equatable { + struct FetchRequest: Codable, Equatable, Hashable { /// The fetch offset of the fetch request. public var offset: UInt8 @@ -57,7 +57,7 @@ public extension LockEvent { } } - struct Predicate: Codable, Equatable { + struct Predicate: Codable, Equatable, Hashable { public var keys: [UUID]? diff --git a/Sources/CoreLock/NewKey.swift b/Sources/CoreLock/NewKey.swift index 50c4f610..80786261 100644 --- a/Sources/CoreLock/NewKey.swift +++ b/Sources/CoreLock/NewKey.swift @@ -42,7 +42,7 @@ public struct NewKey: Codable, Equatable, Hashable, Identifiable { public extension NewKey { /// Exportable new key invitation. - struct Invitation: Codable, Equatable { + struct Invitation: Codable, Equatable, Hashable { /// Identifier of lock. public let lock: UUID diff --git a/Xcode/LockKit/Model/NewKeyInvitationStore.swift b/Xcode/LockKit/Model/NewKeyInvitationStore.swift new file mode 100644 index 00000000..e6f61d4c --- /dev/null +++ b/Xcode/LockKit/Model/NewKeyInvitationStore.swift @@ -0,0 +1,17 @@ +// +// NewKeyInvitationStore.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/21/22. +// + +import Foundation +import CoreLock + +/// Store for managing invitation files +public final class NewKeyInvitationStore { + + internal lazy var fileManager = FileManager() + + public init() { } +} diff --git a/Xcode/LockKit/Model/Predicate.swift b/Xcode/LockKit/Model/Predicate.swift new file mode 100644 index 00000000..74b18677 --- /dev/null +++ b/Xcode/LockKit/Model/Predicate.swift @@ -0,0 +1,23 @@ +// +// Predicate.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/21/22. +// + +import Foundation +import CoreData +import Predicate + +public extension LockEvent.Predicate { + + func toFoundation() -> NSPredicate { + // CoreData predicate + var subpredicates = [Predicate]() + if let keys = self.keys, keys.isEmpty == false { + + } + let predicate: Predicate = subpredicates.isEmpty ? .value(true) : .compound(.and(subpredicates)) + return predicate.toFoundation() + } +} diff --git a/Xcode/LockKit/Model/iCloud/iCloud.swift b/Xcode/LockKit/Model/iCloud/iCloud.swift index 2c3c4547..77b5b8dc 100644 --- a/Xcode/LockKit/Model/iCloud/iCloud.swift +++ b/Xcode/LockKit/Model/iCloud/iCloud.swift @@ -306,7 +306,7 @@ public extension Store { log("☁️ iCloud changed externally") do { - await try self.syncCloud() + try await self.syncCloud() } catch { log("⚠️ Could not sync iCloud: \(error.localizedDescription)") } } diff --git a/Xcode/LockKit/View/EventsView.swift b/Xcode/LockKit/View/EventsView.swift index 24d8b832..c365b9ad 100644 --- a/Xcode/LockKit/View/EventsView.swift +++ b/Xcode/LockKit/View/EventsView.swift @@ -18,7 +18,8 @@ public struct EventsView: View { @Environment(\.managedObjectContext) public var managedObjectContext - public let lock: UUID? + @State + public var predicate: LockEvent.Predicate? @FetchRequest( entity: EventManagedObject.entity(), @@ -49,12 +50,16 @@ public struct EventsView: View { @State private var reloadTask: TaskQueue.PendingTask? + + public init(predicate: LockEvent.Predicate? = nil) { + self.predicate = predicate + } public var body: some View { list .navigationTitle("History") .onAppear { - self._events.wrappedValue.nsPredicate = self.predicate + self._events.wrappedValue.nsPredicate = self.predicate?.toFoundation() } #if os(iOS) .toolbar { @@ -68,26 +73,20 @@ public struct EventsView: View { #endif } - - public init(lock: UUID? = nil) { - self.lock = lock - } } private extension EventsView { - var predicate: NSPredicate? { - lock.flatMap { - NSPredicate( - format: "%K == %@", - #keyPath(EventManagedObject.lock.identifier), - $0 as NSUUID - ) - } - } - + /// Locks to scan for. var locks: Set { - return self.lock.flatMap { [$0] } ?? Set(store.applicationData.locks.keys) + guard let keys = predicate?.keys, keys.isEmpty == false else { + return Set(store.applicationData.locks.keys) // all locks + } + return Set( + store.applicationData.locks + .filter { keys.contains($0.key) } + .map { $0.key } + ) } var list: some View { @@ -218,7 +217,8 @@ private extension EventsView { } let lockName = managedObject.lock?.name ?? "" - if self.lock == nil, lockName.isEmpty == false { + if (self.predicate?.keys?.count ?? 0) == 1, // if filtering for a single lock + lockName.isEmpty == false { keyName = keyName.isEmpty ? lockName : lockName + " - " + keyName } @@ -256,24 +256,23 @@ struct EventsView_Previews: PreviewProvider { @State private var filter = false + private var predicate: LockEvent.Predicate? { + if filter { + return LockEvent.Predicate(keys: [ownerKey]) + } else { + return nil + } + } + var body: some View { #if os(iOS) NavigationView { - EventsView(lock: filter ? lock : nil) - .onAppear(perform: insertLockData) - .navigationBarItems( - leading: Button( - filter ? "Show All" : "Filter", - action: { filter.toggle() } - ), - trailing: Button( - "Insert", - action: insertNewEvents - ) - ) + EventsView(predicate: predicate) } #else - EventsView(lock: filter ? lock : nil) + NavigationStack { + EventsView(predicate: predicate) + } #endif } } diff --git a/Xcode/LockKit/View/KeyDetailView.swift b/Xcode/LockKit/View/KeyDetailView.swift index 123b4cb3..89527f70 100644 --- a/Xcode/LockKit/View/KeyDetailView.swift +++ b/Xcode/LockKit/View/KeyDetailView.swift @@ -57,9 +57,7 @@ public struct KeyDetailView: View { .font(.body) .foregroundColor(.gray) if let schedule = key.permission.schedule { - AppNavigationLink(id: .keySchedule(key.id), destination: { - PermissionScheduleView(schedule: schedule) - }, label: { + AppNavigationLink(id: .keySchedule(schedule), label: { HStack { Text(verbatim: key.permission.localizedText) .foregroundColor(.primary) diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index efcfec7d..59139c96 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -287,9 +287,7 @@ extension LockDetailView { .frame(width: titleWidth, height: nil, alignment: .leading) .font(.body) .foregroundColor(.gray) - AppNavigationLink(id: .events(id), destination: { - EventsView(lock: id) - }, label: { + AppNavigationLink(id: .events(.init(keys: [cache.key.id])), label: { HStack { Text("\(events) events") Image(systemName: "chevron.right") @@ -303,9 +301,7 @@ extension LockDetailView { .frame(width: titleWidth, height: nil, alignment: .leading) .font(.body) .foregroundColor(.gray) - AppNavigationLink(id: .permissions(id), destination: { - PermissionsView(id: id) - }, label: { + AppNavigationLink(id: .permissions(id), label: { HStack { if newKeys > 0 { Text("\(keys) keys, \(newKeys) pending") diff --git a/Xcode/LockKit/View/NavigationLink.swift b/Xcode/LockKit/View/NavigationLink.swift index 81163b9f..7c673804 100644 --- a/Xcode/LockKit/View/NavigationLink.swift +++ b/Xcode/LockKit/View/NavigationLink.swift @@ -6,41 +6,55 @@ // import SwiftUI +import CoreLock -public struct AppNavigationLink : View { +public struct AppNavigationLink : View { public typealias ID = AppNavigationLinkID public let id: ID - private let destination: Destination - private let label: Label public var body: some View { - NavigationLink(destination: destination, label: { label }) + if #available(macOS 13, iOS 16, tvOS 16, *) { + navigationStackLink + } else { + navigationViewLink + } } - public init(id: ID, destination: () -> Destination, label: () -> Label) { + public init(id: ID, label: () -> Label) { self.id = id - self.destination = destination() self.label = label() } } +private extension AppNavigationLink { + + var navigationViewLink: some View { + NavigationLink(destination: { + AppNavigationDestinationView(id: id) + }, label: { + label + }) + } + + @available(macOS 13, iOS 16, tvOS 16, *) + var navigationStackLink: some View { + NavigationLink(value: id, label: { label }) + } +} + // MARK: - Supporting Types public enum AppNavigationLinkID: Hashable { case lock(UUID) - case events(UUID) + case events(LockEvent.Predicate?) case permissions(UUID) - case key(UUID, pending: Bool = false) - case keySchedule(UUID) - - static func newKey(_ id: UUID) -> AppNavigationLinkID { - .key(id, pending: true) - } + case key(KeyDetailView.Value) + case keySchedule(Permission.Schedule) // view only } public enum AppNavigationLinkType: String { @@ -69,3 +83,37 @@ public extension AppNavigationLinkID { } } } + +public struct AppNavigationDestinationView: View { + + public let id: AppNavigationLinkID + + public init(id: AppNavigationLinkID) { + self.id = id + } + + public var body: some View { + switch id { + case let .lock(id): + AnyView( + LockDetailView(id: id) + ) + case let .events(predicate): + AnyView( + EventsView(predicate: predicate) + ) + case let .permissions(id): + AnyView( + PermissionsView(id: id) + ) + case let .key(key): + AnyView( + KeyDetailView(key: key) + ) + case let .keySchedule(schedule): + AnyView( + PermissionScheduleView(schedule: schedule) + ) + } + } +} diff --git a/Xcode/LockKit/View/NewKeyInvitationView.swift b/Xcode/LockKit/View/NewKeyInvitationView.swift new file mode 100644 index 00000000..18183961 --- /dev/null +++ b/Xcode/LockKit/View/NewKeyInvitationView.swift @@ -0,0 +1,39 @@ +// +// NewKeyInvitationView.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/21/22. +// + +import SwiftUI +import CoreLock + +public struct NewKeyInvitationView: View { + + @EnvironmentObject + public var store: Store + + public let newKey: NewKey.Invitation + + public init(newKey: NewKey.Invitation) { + self.newKey = newKey + } + + public var body: some View { + Text("") + } +} + +internal extension NewKeyInvitationView { + + +} + +#if DEBUG +struct NewKeyInvitationView_Previews: PreviewProvider { + static var previews: some View { + //NewKeyInvitationView() + EmptyView() + } +} +#endif diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index bf2a593a..e81e9f42 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -247,9 +247,7 @@ internal extension PermissionsView { private extension PermissionsView.StateView { func row(for item: Key) -> some View { - AppNavigationLink(id: .key(item.id, pending: false), destination: { - destination(for: item) - }, label: { + AppNavigationLink(id: .key(.key(item)), label: { LockRowView( image: .permission(item.permission.type), title: item.name, @@ -263,9 +261,7 @@ private extension PermissionsView.StateView { } func row(for item: NewKey) -> some View { - AppNavigationLink(id: .newKey(item.id), destination: { - destination(for: item) - }, label: { + AppNavigationLink(id: .key(.newKey(item)), label: { LockRowView( image: .permission(item.permission.type), title: item.name, diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index cedb61a0..8a644df1 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -79,6 +79,9 @@ 6E4CB62128D7907200116573 /* PermissionIconViewUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62028D7907200116573 /* PermissionIconViewUIView.swift */; }; 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62228D7907E00116573 /* PermissionIconViewNSView.swift */; }; 6E4CB62528D792BF00116573 /* InfoPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62428D792BF00116573 /* InfoPlist.swift */; }; + 6E6A97EF28DBEC2D00C689F6 /* NewKeyInvitationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */; }; + 6E6A97F128DBEE0700C689F6 /* Predicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97F028DBEE0700C689F6 /* Predicate.swift */; }; + 6E6A97F328DC030000C689F6 /* NewKeyInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */; }; 6E9E37AB28DAA6D100BE7128 /* KeyDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */; }; 6E9E37AD28DADF2600BE7128 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AC28DADF2600BE7128 /* ActivityView.swift */; }; 6E9E37AF28DAEBE000BE7128 /* ActivityItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AE28DAEBE000BE7128 /* ActivityItem.swift */; }; @@ -187,6 +190,9 @@ 6E4CB62028D7907200116573 /* PermissionIconViewUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionIconViewUIView.swift; sourceTree = ""; }; 6E4CB62228D7907E00116573 /* PermissionIconViewNSView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionIconViewNSView.swift; sourceTree = ""; }; 6E4CB62428D792BF00116573 /* InfoPlist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InfoPlist.swift; path = ../../../iOS/SmartLock/InfoPlist.swift; sourceTree = ""; }; + 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyInvitationStore.swift; sourceTree = ""; }; + 6E6A97F028DBEE0700C689F6 /* Predicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Predicate.swift; sourceTree = ""; }; + 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyInvitationView.swift; sourceTree = ""; }; 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyDetailView.swift; sourceTree = ""; }; 6E9E37AC28DADF2600BE7128 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; 6E9E37AE28DAEBE000BE7128 /* ActivityItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityItem.swift; sourceTree = ""; }; @@ -334,6 +340,8 @@ 6E21830B28D7FEF600A622B3 /* FileManager.swift */, 6E21834F28D9506100A622B3 /* Preferences.swift */, 6E21831228D80FDD00A622B3 /* JSON.swift */, + 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */, + 6E6A97F028DBEE0700C689F6 /* Predicate.swift */, 6E21830F28D80DCD00A622B3 /* Keychain.swift */, 6E21831628D8116C00A622B3 /* NewKey.swift */, 6E21831728D8116C00A622B3 /* NewKeyDocument.swift */, @@ -358,6 +366,7 @@ 6E169A7E28D9C135008545EC /* NewPermissionView.swift */, 6E169A8028D9C15B008545EC /* PermissionScheduleView.swift */, 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */, + 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */, ); path = View; sourceTree = ""; @@ -640,6 +649,7 @@ 6E21833B28D8F3B500A622B3 /* ConfirmNewKeyEventManagedObject.swift in Sources */, 6E21830C28D7FEF600A622B3 /* FileManager.swift in Sources */, 6E9E37AB28DAA6D100BE7128 /* KeyDetailView.swift in Sources */, + 6E6A97EF28DBEC2D00C689F6 /* NewKeyInvitationStore.swift in Sources */, 6E4CB61028D7867700116573 /* LockRowView.swift in Sources */, 6E21835028D9506100A622B3 /* Preferences.swift in Sources */, 6E21831828D8116C00A622B3 /* NewKey.swift in Sources */, @@ -679,6 +689,7 @@ 6E21834E28D91FDC00A622B3 /* Event.swift in Sources */, 6E21830E28D7FF2400A622B3 /* AppGroup.swift in Sources */, 6E21836728D9516B00A622B3 /* CloudNewKey.swift in Sources */, + 6E6A97F328DC030000C689F6 /* NewKeyInvitationView.swift in Sources */, 6E21831528D80FF900A622B3 /* ApplicationData.swift in Sources */, 6E21833A28D8F3B500A622B3 /* LockManagedObject.swift in Sources */, 6E21830328D7C47500A622B3 /* Appearance.swift in Sources */, @@ -686,6 +697,7 @@ 6E21833928D8F3B500A622B3 /* ScheduleManagedObject.swift in Sources */, 6E21836028D9516B00A622B3 /* CloudEvent.swift in Sources */, 6E21831328D80FDD00A622B3 /* JSON.swift in Sources */, + 6E6A97F128DBEE0700C689F6 /* Predicate.swift in Sources */, 6E21832128D8501500A622B3 /* ProgressIndicatorView.swift in Sources */, 6E21833828D8F3B500A622B3 /* NewKeyManagedObject.swift in Sources */, 6E4CB61B28D788AA00116573 /* PermissionIconView.swift in Sources */, diff --git a/Xcode/SmartLock/App.swift b/Xcode/SmartLock/App.swift index ab7b8613..8d019df7 100644 --- a/Xcode/SmartLock/App.swift +++ b/Xcode/SmartLock/App.swift @@ -25,6 +25,7 @@ struct LockApp: App { #endif var body: some Scene { + // main window WindowGroup { ContentView() .environmentObject(Store.shared) @@ -36,6 +37,21 @@ struct LockApp: App { } } + + #if os(macOS) + + WindowGroup("Nearby") { + NavigationStack { + NearbyDevicesView() + } + } + + Settings { + NavigationStack { + SettingsView() + } + } + #endif } init() { diff --git a/Xcode/SmartLock/View/SettingsView.swift b/Xcode/SmartLock/View/SettingsView.swift index 03743e9f..ba2a5d02 100644 --- a/Xcode/SmartLock/View/SettingsView.swift +++ b/Xcode/SmartLock/View/SettingsView.swift @@ -8,16 +8,23 @@ import SwiftUI struct SettingsView: View { + var body: some View { - Text("Settings") + List { + Text("Setting 1") + Text("Setting 2") + } .navigationTitle("Settings") } } +#if DEBUG +@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) struct SettingsView_Previews: PreviewProvider { static var previews: some View { - NavigationView { + NavigationStack { SettingsView() } } } +#endif diff --git a/Xcode/SmartLock/View/SidebarLabel.swift b/Xcode/SmartLock/View/SidebarLabel.swift index 314d8aca..7dab7fb8 100644 --- a/Xcode/SmartLock/View/SidebarLabel.swift +++ b/Xcode/SmartLock/View/SidebarLabel.swift @@ -8,6 +8,7 @@ #if os(macOS) import SwiftUI import LockKit +import SFSafeSymbols struct SidebarLabel: View { @@ -30,7 +31,7 @@ extension SidebarLabel { case loading case permission(PermissionType) case emoji(Character) - case symbol(String) + case symbol(SFSymbol) } } @@ -58,7 +59,7 @@ extension SidebarLabel { ) case let .symbol(symbol): AnyView( - SwiftUI.Image(systemName: symbol) + SwiftUI.Image(systemSymbol: symbol) .font(.system(size: 15)) ) } @@ -88,7 +89,7 @@ struct SidebarLabel_Previews: PreviewProvider { SidebarLabel(title: "Lock 3", image: .permission(.scheduled)) SidebarLabel(title: "Lock", image: .permission(.anytime)) }, label: { - SidebarLabel(title: "Nearby", image: .symbol("antenna.radiowaves.left.and.right")) + SidebarLabel(title: "Nearby", image: .symbol(.antennaRadiowavesLeftAndRight)) // "antenna.radiowaves.left.and.right" }) DisclosureGroup(content: { SidebarLabel(title: "Setup", image: .permission(.owner)) @@ -96,7 +97,7 @@ struct SidebarLabel_Previews: PreviewProvider { SidebarLabel(title: "Lock 2", image: .permission(.anytime)) SidebarLabel(title: "Lock 2", image: .permission(.scheduled)) }, label: { - SidebarLabel(title: "Keys", image: .symbol("key")) + SidebarLabel(title: "Keys", image: .symbol(.key)) }) } } diff --git a/Xcode/SmartLock/View/SidebarView.swift b/Xcode/SmartLock/View/SidebarView.swift index 4f7ad9f8..74e9e161 100644 --- a/Xcode/SmartLock/View/SidebarView.swift +++ b/Xcode/SmartLock/View/SidebarView.swift @@ -8,6 +8,7 @@ #if os(macOS) import SwiftUI import LockKit +import SFSafeSymbols struct SidebarView: View { @@ -23,6 +24,12 @@ struct SidebarView: View { @State private var isKeysExpanded = true + @State + private var isEventsExpanded = true + + @State + private var isNewKeysExpanded = true + @State private var detail: AnyView = AnyView(Text("Select a lock")) @@ -36,15 +43,21 @@ struct SidebarView: View { scanStatus: scanStatus, locks: locks, keys: keys, + events: events, isNearbyExpanded: Binding( get: { isNearbyExpanded }, set: { toggleNearbyExpanded($0) } ), isKeysExpanded: $isKeysExpanded, + isEventsExpanded: $isEventsExpanded, + isNewKeysExpanded: $isNewKeysExpanded, toggleScan: toggleScan ) NavigationStack { detail + .navigationDestination(for: AppNavigationLinkID.self) { + AppNavigationDestinationView(id: $0) + } } } .navigationViewStyle(.columns) @@ -86,6 +99,10 @@ private extension SidebarView { .map { .key($0.key.id, $0.name, $0.key.permission.type) } } + var events: [Item] { + [] // FIXME: Events + } + func toggleScan() { if store.isScanning { store.stopScanning() @@ -150,8 +167,9 @@ private extension SidebarView { assertionFailure("Selected unknown key \(keyID)") return } - detail = AnyView(detailView(for: lock)) + case let .events(predicate, _): + detail = AnyView(EventsView(predicate: predicate)) } } @@ -179,37 +197,6 @@ private extension SidebarView { return .lock(peripheral.id, "Loading...", nil) } } - - var selectionDetail: AnyView { - guard let selection = self.sidebarSelection, let item = locks.first(where: { $0.id == selection }) ?? keys.first(where: { $0.id == selection }) else { - return AnyView( - Text("Select a lock") - ) - } - switch item { - case let .lock(id, _, _): - // FIXME: store peripheral instead of id - guard let peripheral = store.peripherals.keys.first(where: { $0.id == id }) else { - return AnyView( - Text("Select a lock") - ) - } - guard let information = store.lockInformation[peripheral] else { - return AnyView( - ProgressView() - .progressViewStyle(.circular) - ) - } - return AnyView(LockDetailView(id: information.id)) - case let .key(id, _, _): - guard let lock = store.applicationData.locks.first(where: { $0.value.key.id == id })?.key else { - return AnyView( - Text("Select a lock") - ) - } - return AnyView(LockDetailView(id: lock)) - } - } } extension SidebarView { @@ -225,28 +212,48 @@ extension SidebarView { let keys: [Item] + let events: [Item] + @Binding var isNearbyExpanded: Bool @Binding var isKeysExpanded: Bool + @Binding + var isEventsExpanded: Bool + + @Binding + var isNewKeysExpanded: Bool + var toggleScan: () -> () var body: some View { List(selection: $selection) { Group( title: "Nearby", - image: scanStatus == .scanning ? .loading : .symbol("antenna.radiowaves.left.and.right"), + image: scanStatus == .scanning ? .loading : .symbol(.antennaRadiowavesLeftAndRight), //"antenna.radiowaves.left.and.right" items: locks, isExpanded: $isNearbyExpanded ) Group( title: "Keys", - image: .symbol("key"), + image: .symbol(.key), items: keys, isExpanded: $isKeysExpanded ) + Group( + title: "Invitations", + image: .symbol(.envelope), + items: [], + isExpanded: $isNewKeysExpanded + ) + Group( + title: "History", + image: .symbol(.clock), + items: events, + isExpanded: $isEventsExpanded + ) } .toolbar { ToolbarItem(placement: .primaryAction) { @@ -318,6 +325,8 @@ extension SidebarView { enum Item { case lock(NativePeripheral.ID, String, PermissionType?) case key(UUID, String, PermissionType) + //case newKey(URL) + case events(LockEvent.Predicate?, String) } } @@ -329,6 +338,8 @@ extension SidebarView.Item: Identifiable { return "lock_" + id.description case let .key(id, _, _): return "key_" + id.description + case let .events(predicate, _): + return "events_\(predicate.flatMap { "\($0)" } ?? "all")" } } } @@ -347,15 +358,19 @@ extension SidebarLabel { title: title, image: .permission(permission) ) + case let .events(_, name): + self.init(title: name, image: .symbol(.clock)) } } } // MARK: - Preview +#if DEBUG struct Sidebar_Previews: PreviewProvider { static var previews: some View { EmptyView() } } #endif +#endif diff --git a/Xcode/SmartLock/View/TabBarView.swift b/Xcode/SmartLock/View/TabBarView.swift index 98ab2b3d..471469b7 100644 --- a/Xcode/SmartLock/View/TabBarView.swift +++ b/Xcode/SmartLock/View/TabBarView.swift @@ -18,7 +18,7 @@ struct TabBarView: View { Text("Select a lock") } .tabItem { - Label("Nearby", systemImage: "location.circle.fill") + Label("Nearby", systemSymbol: .locationCircleFill) } // Keys @@ -27,7 +27,7 @@ struct TabBarView: View { Text("Select a lock") } .tabItem { - Label("Keys", systemImage: "key.fill") + Label("Keys", systemSymbol: .keyFill) } // History @@ -35,7 +35,7 @@ struct TabBarView: View { EventsView() } .tabItem { - Label("History", systemImage: "clock.fill") + Label("History", systemSymbol: .clockFill) } // Settings @@ -44,7 +44,7 @@ struct TabBarView: View { Text("Settings detail") } .tabItem { - Label("Settings", systemImage: "gearshape.fill") + Label("Settings", systemSymbol: .gearshapeFill) } } .navigationViewStyle(.stack) From d3d77ae50cc5e97bd84fa0022e5231d26bbd0cf3 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 21 Sep 2022 22:59:55 -0700 Subject: [PATCH 129/229] [App] Added `TabBarView.SplitView` --- Xcode/LockKit/View/EventsView.swift | 4 +- Xcode/LockKit/View/LockDetailView.swift | 117 +++++++++++------- Xcode/LockKit/View/NavigationLink.swift | 3 + Xcode/LockKit/View/NewPermissionView.swift | 10 +- .../LockKit/View/PermissionScheduleView.swift | 2 +- Xcode/LockKit/View/PermissionsView.swift | 10 -- Xcode/SmartLock.xcodeproj/project.pbxproj | 4 + Xcode/SmartLock/App.swift | 69 ++++++++--- Xcode/SmartLock/View/NearbyDevicesView.swift | 13 +- Xcode/SmartLock/View/TabBarView.swift | 95 +++++++++++--- 10 files changed, 231 insertions(+), 96 deletions(-) diff --git a/Xcode/LockKit/View/EventsView.swift b/Xcode/LockKit/View/EventsView.swift index c365b9ad..22b677e5 100644 --- a/Xcode/LockKit/View/EventsView.swift +++ b/Xcode/LockKit/View/EventsView.swift @@ -93,13 +93,15 @@ private extension EventsView { List(events) { row(for: $0) } - .listStyle(.plain) .refreshable { reload() } .onAppear { reload() } + #if os(iOS) + .listStyle(.plain) + #endif } func reload() { diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 59139c96..58c165ec 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -25,6 +25,13 @@ public struct LockDetailView: View { @State private var pendingTask: TaskQueue.PendingTask? + @State + private var showNewKeyModal = false + + public init(id: UUID) { + self.id = id + } + public var body: some View { if let cache = self.cache { AnyView( @@ -42,28 +49,50 @@ public struct LockDetailView: View { .onAppear { reload() } - #if os(iOS) .toolbar { + #if os(iOS) ToolbarItem(placement: .navigationBarTrailing) { if activityIndicator { ProgressView() .progressViewStyle(.circular) } } + #endif + + ToolbarItem(placement: .primaryAction) { + Button(action: { }) { + Image(systemSymbol: .pencil) + } + } + + ToolbarItem(placement: .primaryAction) { + Button(action: { }) { + Image(systemSymbol: .trash) + } + } + + // create new key + ToolbarItem(placement: .primaryAction) { + if cache.key.permission.isAdministrator { + Button(action: newPermission) { + Image(systemSymbol: .plus) + } + } + } + } - #endif ) } else if let information = self.information, information.status == .setup { - AnyView(Text("Setup")) + #if os(iOS) + AnyView(SetupLockView(id: id)) + #else + AnyView(Text("Scan to Setup")) + #endif } else { AnyView(UnknownView(id: id, information: information)) } } - - public init(id: UUID) { - self.id = id - } } private extension LockDetailView { @@ -76,41 +105,6 @@ private extension LockDetailView { store.lockInformation.first(where: { $0.value.id == id })?.value } - func unlock() { - // FIXME: Handler errors - Task { - guard await store.central.state == .poweredOn else { - return - } - // FaceID - let authentication = LAContext() - let policy: LAPolicy = .deviceOwnerAuthenticationWithBiometrics - let reason = NSLocalizedString("Biometrics are needed to unlock", comment: "") - do { - if try authentication.canEvaluate(policy: policy) { - try await authentication.evaluatePolicy(policy, localizedReason: reason) - } - } - catch { - log("⚠️ Unable to authenticate for unlock \(id). \(error)") - } - // cancel all operation - if store.isScanning { - store.stopScanning() - } - await TaskQueue.bluetooth.cancelAll() - await pendingTask?.cancel() - pendingTask = await Task.bluetooth { - do { - // Bluetooth request - try await store.unlock(for: id, action: .default) - } catch { - log("⚠️ Unable to unlock \(id). \(error)") - } - } - } - } - var events: Int { let fetchRequest = EventManagedObject.fetchRequest() fetchRequest.predicate = NSPredicate( @@ -141,6 +135,10 @@ private extension LockDetailView { return (try? managedObjectContext.count(for: fetchRequest)) ?? 0 } + func newPermission() { + showNewKeyModal = true + } + func reload() { let lock = self.id activityIndicator = true @@ -188,6 +186,41 @@ private extension LockDetailView { } } } + + func unlock() { + // FIXME: Handle errors + Task { + guard await store.central.state == .poweredOn else { + return + } + // FaceID + let authentication = LAContext() + let policy: LAPolicy = .deviceOwnerAuthenticationWithBiometrics + let reason = NSLocalizedString("Biometrics are needed to unlock", comment: "") + do { + if try authentication.canEvaluate(policy: policy) { + try await authentication.evaluatePolicy(policy, localizedReason: reason) + } + } + catch { + log("⚠️ Unable to authenticate for unlock \(id). \(error)") + } + // cancel all operation + if store.isScanning { + store.stopScanning() + } + await TaskQueue.bluetooth.cancelAll() + await pendingTask?.cancel() + pendingTask = await Task.bluetooth { + do { + // Bluetooth request + try await store.unlock(for: id, action: .default) + } catch { + log("⚠️ Unable to unlock \(id). \(error)") + } + } + } + } } extension LockDetailView { diff --git a/Xcode/LockKit/View/NavigationLink.swift b/Xcode/LockKit/View/NavigationLink.swift index 7c673804..9477a4f0 100644 --- a/Xcode/LockKit/View/NavigationLink.swift +++ b/Xcode/LockKit/View/NavigationLink.swift @@ -43,6 +43,9 @@ private extension AppNavigationLink { @available(macOS 13, iOS 16, tvOS 16, *) var navigationStackLink: some View { NavigationLink(value: id, label: { label }) + .navigationDestination(for: AppNavigationLinkID.self) { + AppNavigationDestinationView(id: $0) + } } } diff --git a/Xcode/LockKit/View/NewPermissionView.swift b/Xcode/LockKit/View/NewPermissionView.swift index 49ef6386..afe6c8cd 100644 --- a/Xcode/LockKit/View/NewPermissionView.swift +++ b/Xcode/LockKit/View/NewPermissionView.swift @@ -154,15 +154,21 @@ private extension NewPermissionView.StateView { } var createToolbarItem: some ToolbarContent { - ToolbarItem(placement: .confirmationAction) { + ToolbarItem(placement: .primaryAction) { if state == .loading { + #if os(macOS) + AnyView( + ProgressIndicatorView(style: .spinning, controlSize: .mini) + ) + #else AnyView( ProgressView() .progressViewStyle(.circular) ) + #endif } else { AnyView( - Button("Create", action: { create() }) + Button("Create", action: create) ) } } diff --git a/Xcode/LockKit/View/PermissionScheduleView.swift b/Xcode/LockKit/View/PermissionScheduleView.swift index cde19065..007522e2 100644 --- a/Xcode/LockKit/View/PermissionScheduleView.swift +++ b/Xcode/LockKit/View/PermissionScheduleView.swift @@ -18,7 +18,7 @@ public struct PermissionScheduleView: View { public init(schedule: Permission.Schedule = .init()) { isEditable = false - _schedule = Binding(get: { schedule }, set: { _ in }) // read only + _schedule = .constant(schedule) } public init(schedule: Binding) { diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index e81e9f42..5c141feb 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -88,16 +88,6 @@ public struct PermissionsView: View { Button(action: newPermission, label: { Image(systemSymbol: .plus) }) - #if os(iOS) - .popover(item: $newKeyInvitation, content: { key in - ActivityView( - activityItems: newKeyInvitation - .flatMap { [NewKeyFileActivityItem(invitation: $0)] as [Any] } ?? [], - applicationActivities: nil, - excludedActivityTypes: NewKeyFileActivityItem.excludedActivityTypes - ) - }) - #endif } } #if os(iOS) diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 8a644df1..e490eac8 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -82,6 +82,7 @@ 6E6A97EF28DBEC2D00C689F6 /* NewKeyInvitationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */; }; 6E6A97F128DBEE0700C689F6 /* Predicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97F028DBEE0700C689F6 /* Predicate.swift */; }; 6E6A97F328DC030000C689F6 /* NewKeyInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */; }; + 6E8BBFDA28DC2CA000F03735 /* SetupLockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */; }; 6E9E37AB28DAA6D100BE7128 /* KeyDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */; }; 6E9E37AD28DADF2600BE7128 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AC28DADF2600BE7128 /* ActivityView.swift */; }; 6E9E37AF28DAEBE000BE7128 /* ActivityItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AE28DAEBE000BE7128 /* ActivityItem.swift */; }; @@ -193,6 +194,7 @@ 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyInvitationStore.swift; sourceTree = ""; }; 6E6A97F028DBEE0700C689F6 /* Predicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Predicate.swift; sourceTree = ""; }; 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyInvitationView.swift; sourceTree = ""; }; + 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupLockView.swift; sourceTree = ""; }; 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyDetailView.swift; sourceTree = ""; }; 6E9E37AC28DADF2600BE7128 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; 6E9E37AE28DAEBE000BE7128 /* ActivityItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityItem.swift; sourceTree = ""; }; @@ -367,6 +369,7 @@ 6E169A8028D9C15B008545EC /* PermissionScheduleView.swift */, 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */, 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */, + 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */, ); path = View; sourceTree = ""; @@ -674,6 +677,7 @@ 6E21836528D9516B00A622B3 /* CloudLock.swift in Sources */, 6E21834228D8F3B500A622B3 /* UnlockEventManagedObject.swift in Sources */, 6E21833528D8F3B500A622B3 /* SetupEventManagedObject.swift in Sources */, + 6E8BBFDA28DC2CA000F03735 /* SetupLockView.swift in Sources */, 6E9E37AD28DADF2600BE7128 /* ActivityView.swift in Sources */, 6E21834328D8F3B500A622B3 /* ManagedObject.swift in Sources */, 6E21834028D8F3B500A622B3 /* RemoveKeyEventManagedObject.swift in Sources */, diff --git a/Xcode/SmartLock/App.swift b/Xcode/SmartLock/App.swift index 8d019df7..36f99be7 100644 --- a/Xcode/SmartLock/App.swift +++ b/Xcode/SmartLock/App.swift @@ -21,7 +21,8 @@ struct LockApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate #elseif os(iOS) || os(tvOS) - + @UIApplicationDelegateAdaptor(AppDelegate.self) + var appDelegate #endif var body: some Scene { @@ -30,26 +31,37 @@ struct LockApp: App { ContentView() .environmentObject(Store.shared) .environment(\.managedObjectContext, Store.shared.managedObjectContext) - .onAppear { - _ = LockApp.initialize - } - .onContinueUserActivity("") { _ in - - } } #if os(macOS) - - WindowGroup("Nearby") { + Window("Nearby", id: "nearby") { NavigationStack { NearbyDevicesView() + .navigationDestination(for: AppNavigationLinkID.self) { + AppNavigationDestinationView(id: $0) + } + } + .environmentObject(Store.shared) + .environment(\.managedObjectContext, Store.shared.managedObjectContext) + } + + Window("Keys", id: "keys") { + NavigationStack { + KeysView() + .navigationDestination(for: AppNavigationLinkID.self) { + AppNavigationDestinationView(id: $0) + } } + .environmentObject(Store.shared) + .environment(\.managedObjectContext, Store.shared.managedObjectContext) } Settings { NavigationStack { SettingsView() } + .environmentObject(Store.shared) + .environment(\.managedObjectContext, Store.shared.managedObjectContext) } #endif } @@ -58,19 +70,44 @@ struct LockApp: App { // print app info log("Launching SmartLock v\(Bundle.InfoPlist.shortVersion) (\(Bundle.InfoPlist.version))") } +} + +#if os(iOS) +final class AppDelegate: UIResponder, UIApplicationDelegate { - static let initialize: () = { - #if canImport(UIKit) + // MARK: - UIApplicationDelegate + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + // set app appearance //UIView.configureLockAppearance() - #endif - }() + + return true + } + + func applicationDidBecomeActive( + _ application: UIApplication + ) { + + Task { + do { try await Store.shared.syncCloud() } + catch { log("⚠️ Unable to automatically sync with iCloud. \(error)") } + } + } } - -#if os(macOS) +#elseif os(macOS) final class AppDelegate: NSResponder, NSApplicationDelegate { - func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + // MARK: - NSApplicationDelegate + + + + func applicationShouldTerminateAfterLastWindowClosed( + _ sender: NSApplication + ) -> Bool { return false } } diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index 5c68bc95..0b3b5e80 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -121,10 +121,13 @@ extension NearbyDevicesView { #if os(iOS) list .navigationTitle(title) - .navigationBarItems(trailing: trailingButtonItem) + .navigationBarItems(trailing: scanButton) #elseif os(macOS) list .navigationTitle(title) + .toolbar { + ToolbarItem(id: "scan", placement: .primaryAction) { scanButton } + } #endif } } @@ -153,19 +156,19 @@ private extension NearbyDevicesView.StateView { } } - var trailingButtonItem: some View { + var scanButton: some View { Button(action: { toggleScan() }, label: { switch state { case .bluetoothUnavailable: - Image(systemName: "exclamationmark.triangle.fill") + Image(systemSymbol: .exclamationmarkTriangleFill) //"exclamationmark.triangle.fill" .symbolRenderingMode(.multicolor) case .scanning: - Image(systemName: "stop.fill") + Image(systemSymbol: .stopFill) // "stop.fill" .symbolRenderingMode(.monochrome) case .stopScan: - Image(systemName: "arrow.clockwise") + Image(systemSymbol: .arrowClockwise) // "arrow.clockwise" .symbolRenderingMode(.monochrome) } }) diff --git a/Xcode/SmartLock/View/TabBarView.swift b/Xcode/SmartLock/View/TabBarView.swift index 471469b7..7ce1e6a6 100644 --- a/Xcode/SmartLock/View/TabBarView.swift +++ b/Xcode/SmartLock/View/TabBarView.swift @@ -8,27 +8,31 @@ #if os(iOS) import SwiftUI import LockKit +import SFSafeSymbols struct TabBarView: View { + + //@State + //private var columnVisibility: NavigationSplitViewVisibility = .doubleColumn + var body: some View { TabView { + // Nearby - NavigationView { - NearbyDevicesView() - Text("Select a lock") - } - .tabItem { - Label("Nearby", systemSymbol: .locationCircleFill) - } + SplitView( + title: "Nearby", + systemSymbol: .locationCircleFill, + sidebar: { NearbyDevicesView() }, + detail: { Text("Select a lock") } + ) // Keys - NavigationView { - KeysView() - Text("Select a lock") - } - .tabItem { - Label("Keys", systemSymbol: .keyFill) - } + SplitView( + title: "Keys", + systemSymbol: .keyFill, + sidebar: { KeysView() }, + detail: { Text("Select a lock") } + ) // History NavigationView { @@ -37,6 +41,7 @@ struct TabBarView: View { .tabItem { Label("History", systemSymbol: .clockFill) } + .navigationViewStyle(.stack) // Settings NavigationView { @@ -46,21 +51,73 @@ struct TabBarView: View { .tabItem { Label("Settings", systemSymbol: .gearshapeFill) } + .navigationViewStyle(.stack) } - .navigationViewStyle(.stack) .navigationBarTitleDisplayMode(.large) - .onAppear { - Task { - do { try await Store.shared.syncCloud() } - catch { log("⚠️ Unable to automatically sync with iCloud. \(error)") } + } +} + +extension TabBarView { + + struct SplitView : View { + + let title: LocalizedStringKey + + let systemSymbol: SFSymbol + + let sidebar: () -> Sidebar + + let detail: () -> Detail + + @State + private var columnVisibilityData: Data? + + var body: some View { + navigationView + .tabItem { + Label(title, systemSymbol: systemSymbol) + } + } + } +} + +private extension TabBarView.SplitView { + + var navigationView: some View { + if #available(iOS 16.0, *) { + return NavigationSplitView( + columnVisibility: columnVisibility, + sidebar: sidebar, + detail: detail + ) + .navigationSplitViewStyle(.prominentDetail) + } else { + return NavigationView { + sidebar() + detail() } + .navigationViewStyle(.stack) } } + + @available(iOS 16.0, *) + var columnVisibility: Binding { + Binding(get: { + let decoder = JSONDecoder() + return columnVisibilityData.flatMap { try? decoder.decode(NavigationSplitViewVisibility.self, from: $0) } ?? .automatic + }, set: { + let encoder = JSONEncoder() + columnVisibilityData = try? encoder.encode($0) + }) + } } +#if DEBUG struct TabBarView_Previews: PreviewProvider { static var previews: some View { TabBarView() } } #endif + +#endif From 4711df686fafb73e206dafc2a0c10b0a689b4323 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 21 Sep 2022 23:00:14 -0700 Subject: [PATCH 130/229] [App] Added `SetupLockView` --- Xcode/LockKit/View/SetupLockView.swift | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 Xcode/LockKit/View/SetupLockView.swift diff --git a/Xcode/LockKit/View/SetupLockView.swift b/Xcode/LockKit/View/SetupLockView.swift new file mode 100644 index 00000000..3782c916 --- /dev/null +++ b/Xcode/LockKit/View/SetupLockView.swift @@ -0,0 +1,31 @@ +// +// SetupLockView.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/21/22. +// + +import SwiftUI +import CoreLock + +/// View for lock setup. +public struct SetupLockView: View { + + public let id: UUID + + public init(id: UUID) { + self.id = id + } + + public var body: some View { + Text("Setup this lock on your iOS device.") + } +} + +#if DEBUG +struct SetupLockView_Previews: PreviewProvider { + static var previews: some View { + EmptyView() + } +} +#endif From 2943bee2f5ad23274b2c95ced5a1ce8d126ea203 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 22 Sep 2022 11:38:39 -0700 Subject: [PATCH 131/229] [App] Added `View.newPermissionSheet()` --- Xcode/LockKit/Model/FileManager.swift | 4 ++ Xcode/LockKit/Model/NewKeyDocument.swift | 70 +++++++++++++++------- Xcode/LockKit/View/LockDetailView.swift | 11 +++- Xcode/LockKit/View/NewPermissionView.swift | 44 ++++++++++++++ Xcode/LockKit/View/PermissionsView.swift | 33 ++-------- 5 files changed, 114 insertions(+), 48 deletions(-) diff --git a/Xcode/LockKit/Model/FileManager.swift b/Xcode/LockKit/Model/FileManager.swift index 5b3c8b6c..b3f69308 100644 --- a/Xcode/LockKit/Model/FileManager.swift +++ b/Xcode/LockKit/Model/FileManager.swift @@ -19,6 +19,10 @@ public extension FileManager { var cachesDirectory: URL? { return urls(for: .cachesDirectory, in: .userDomainMask).first } + + var documentsURL: URL? { + return urls(for: .documentDirectory, in: .userDomainMask).first + } } public extension FileManager { diff --git a/Xcode/LockKit/Model/NewKeyDocument.swift b/Xcode/LockKit/Model/NewKeyDocument.swift index e5e6a87e..62621994 100644 --- a/Xcode/LockKit/Model/NewKeyDocument.swift +++ b/Xcode/LockKit/Model/NewKeyDocument.swift @@ -6,34 +6,64 @@ // Copyright © 2019 ColemanCDA. All rights reserved. // -#if canImport(UIKit) import Foundation import CoreLock -import UIKit +import SwiftUI +import UniformTypeIdentifiers -/// New Key Document -public final class NewKeyDocument: UIDocument { - - // MARK: - Properties - - public private(set) var invitation: NewKey.Invitation? - - private lazy var encoder = JSONEncoder() +@available(iOS 14.0, macOS 11.0, *) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +public extension NewKey.Invitation { - private lazy var decoder = JSONDecoder() - - // MARK: - Methods - - public override func contents(forType typeName: String) throws -> Any { + /// New Key Invitation File Document + struct Document: FileDocument { + + /// The types the document is able to open. + public static var readableContentTypes: [UTType] { + return [.json] + } + + public init(configuration: ReadConfiguration) throws { + fatalError() + } - guard let invitation = self.invitation else { return Data() } - return try encoder.encode(invitation) + public func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + + fatalError() + } } +} + +#if canImport(UIKit) +import UIKit + +public extension UIDocument { - public override func load(fromContents contents: Any, ofType typeName: String?) throws { + /// New Key Invitation Document + final class NewKeyInvitation: UIDocument { + + // MARK: - Properties + + public private(set) var invitation: NewKey.Invitation? + + private lazy var encoder = JSONEncoder() + + private lazy var decoder = JSONDecoder() + + // MARK: - Methods + + public override func contents(forType typeName: String) throws -> Any { + + guard let invitation = self.invitation else { return Data() } + return try encoder.encode(invitation) + } - guard let data = contents as? Data else { return } - self.invitation = try decoder.decode(NewKey.Invitation.self, from: data) + public override func load(fromContents contents: Any, ofType typeName: String?) throws { + + guard let data = contents as? Data else { return } + self.invitation = try decoder.decode(NewKey.Invitation.self, from: data) + } } } #endif diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 58c165ec..909486af 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -49,6 +49,12 @@ public struct LockDetailView: View { .onAppear { reload() } + .newPermissionSheet( + for: id, + isPresented: $showNewKeyModal, + onDismiss: { }, + completion: didCreateNewKey + ) .toolbar { #if os(iOS) ToolbarItem(placement: .navigationBarTrailing) { @@ -79,7 +85,6 @@ public struct LockDetailView: View { } } } - } ) } else if let information = self.information, @@ -139,6 +144,10 @@ private extension LockDetailView { showNewKeyModal = true } + func didCreateNewKey(_ newKey: NewKey.Invitation) { + + } + func reload() { let lock = self.id activityIndicator = true diff --git a/Xcode/LockKit/View/NewPermissionView.swift b/Xcode/LockKit/View/NewPermissionView.swift index afe6c8cd..90af5f31 100644 --- a/Xcode/LockKit/View/NewPermissionView.swift +++ b/Xcode/LockKit/View/NewPermissionView.swift @@ -290,6 +290,50 @@ private extension NewPermissionView.PermissionTypeView { } } +// MARK: - View Extensions + +public extension View { + + func newPermissionSheet( + for lock: UUID, + isPresented: Binding, + onDismiss: @escaping () -> (), + completion: @escaping (NewKey.Invitation) -> () + ) -> some View { + return self.sheet(isPresented: isPresented, onDismiss: onDismiss) { + #if os(iOS) + NavigationView { + NewPermissionView(id: lock, completion: completion) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + isPresented.wrappedValue = false + } + } + } + } + #elseif os(macOS) + NavigationStack { + ScrollView { + NewPermissionView(id: lock, completion: completion) + .padding(20) + } + } + .frame(width: 500, height: 500) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + isPresented.wrappedValue = false + } + } + } + #endif + } + } +} + +// MARK: - Preview + #if DEBUG struct NewPermissionView_Previews: PreviewProvider { static var previews: some View { diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index 5c141feb..aae70d86 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -99,33 +99,12 @@ public struct PermissionsView: View { } #endif } - .sheet(isPresented: $showNewKeyModal, onDismiss: { }) { - #if os(iOS) - NavigationView { - NewPermissionView(id: id, completion: didCreateNewKey) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - showNewKeyModal = false - } - } - } - } - #elseif os(macOS) - ScrollView { - NewPermissionView(id: id, completion: didCreateNewKey) - .padding(30) - } - .frame(width: 500, height: 500) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - showNewKeyModal = false - } - } - } - #endif - } + .newPermissionSheet( + for: id, + isPresented: $showNewKeyModal, + onDismiss: { }, + completion: didCreateNewKey + ) } public init(id: UUID) { From fe4bdca0e98e6f68626062cac4fb587a1f6253e6 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 22 Sep 2022 11:38:51 -0700 Subject: [PATCH 132/229] [App] Updated `NewKeyInvitationStore` --- .../LockKit/Model/NewKeyInvitationStore.swift | 93 ++++++++++++++++++- 1 file changed, 90 insertions(+), 3 deletions(-) diff --git a/Xcode/LockKit/Model/NewKeyInvitationStore.swift b/Xcode/LockKit/Model/NewKeyInvitationStore.swift index e6f61d4c..a4109e2a 100644 --- a/Xcode/LockKit/Model/NewKeyInvitationStore.swift +++ b/Xcode/LockKit/Model/NewKeyInvitationStore.swift @@ -8,10 +8,97 @@ import Foundation import CoreLock -/// Store for managing invitation files -public final class NewKeyInvitationStore { +/// Store for managing invitation files that you created. +public final class NewKeyInvitationStore: ObservableObject { + + public typealias Cache = [URL: NewKey.Invitation] + + @Published + public private(set) var cache = Cache() internal lazy var fileManager = FileManager() - public init() { } + public static let shared = NewKeyInvitationStore() + + private init() { } + + @discardableResult + public func save( + _ invitation: NewKey.Invitation, + fileName: String? = nil + ) async throws -> URL { + let documentsURL = try self.documentsURL + let fileName = fileName ?? "newKey-\(invitation.key.id).ekey" + let fileURL = documentsURL.appendingPathComponent(fileName) + let encoder = JSONEncoder() + let writeTask = Task { + let data = try encoder.encode(invitation) + try data.write(to: fileURL, options: [.atomic]) + } + // wait for task + try await writeTask.value + await self.cache(invitation, url: fileURL) + } + + @discardableResult + public func fetchAll() async throws -> Cache { + let documentsURL = try self.documentsURL + let files = try fileManager.contentsOfDirectory(atPath: documentsURL.path) + let keyFiles = files.filter { $0.hasSuffix(".ekey") } + + // attempt to read concurrently + let oldValue = await MainActor.run { self.cache } + let newValue = await withTaskGroup(of: (URL, Result).self, returning: Cache.self) { taskGroup in + for path in keyFiles { + let url = URL(fileURLWithPath: path) + taskGroup.addTask { + do { + let decoder = JSONDecoder() + let data = try Data(contentsOf: url, options: [.mappedIfSafe]) + let value = try decoder.decode(NewKey.Invitation.self, from: data) + // update UI incrementally + await self.cache(value, url: url) + return (url, .success(value)) + } + catch { + log("⚠️ Unable to read \(url.lastPathComponent). \(error.localizedDescription)") + return (url, .failure(error)) + } + } + } + + // build result serially + var newValue = Cache() + newValue.reserveCapacity(oldValue.count + 2) + for await value in taskGroup { + switch value { + case let .success(newKey): + newValue[newKey] + case let .failure(error): + // decrement count + break + } + } + return newValue + } + // replace everything + await MainActor.run { + self.cache = newValue + } + return newValue + } + + internal var documentsURL: URL { + get throws { + guard let url = fileManager.documentsURL else { + throw CocoaError(.fileNoSuchFile) + } + return url + } + } + + @MainActor + private func cache(_ value: NewKey.Invitation, url: URL) async { + self.cache[url] = value + } } From 37c3581845833558504ca6fcc57ea99a6018b457 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 22 Sep 2022 17:05:06 -0700 Subject: [PATCH 133/229] [App] Added `AsyncFetchRequest` --- Xcode/LockKit/Model/AsyncFetchRequest.swift | 193 ++++++++++++++++++ .../LockKit/Model/NewKeyInvitationStore.swift | 5 +- Xcode/SmartLock.xcodeproj/project.pbxproj | 4 + 3 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 Xcode/LockKit/Model/AsyncFetchRequest.swift diff --git a/Xcode/LockKit/Model/AsyncFetchRequest.swift b/Xcode/LockKit/Model/AsyncFetchRequest.swift new file mode 100644 index 00000000..6297154a --- /dev/null +++ b/Xcode/LockKit/Model/AsyncFetchRequest.swift @@ -0,0 +1,193 @@ +// +// AsyncFetchRequest.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/22/22. +// + +import Foundation +import SwiftUI + +@propertyWrapper +@MainActor +public struct AsyncFetchRequest { + + internal let dataSource: DataSource + + internal let configuration: DataSource.Configuration + + public init( + dataSource: DataSource, + configuration: DataSource.Configuration + ) { + self.dataSource = dataSource + self.configuration = configuration + } + + @MainActor + public var wrappedValue: AsyncFetchedResults { + return .init( + dataSource: dataSource, + configuration: configuration + ) + } +} + +public protocol AsyncFetchDataSource: AnyObject, ObservableObject { + + associatedtype ID: Hashable + + associatedtype Success + + associatedtype Failure: Error + + associatedtype Configuration + + /// Provide the cached result if value has been fetched. + func cachedValue(for id: ID) -> Success? + + /// Provide sorted and filtered results. + func fetch(configuration: Configuration) -> [ID] + + /// Asyncronously load the specified item. + func load(id: ID) async -> Result +} + +extension AsyncFetchRequest: DynamicProperty { + + //public mutating func update() { +} + +@MainActor +public struct AsyncFetchedResults { + + @ObservedObject + internal var dataSource: DataSource + + internal var configuration: DataSource.Configuration + + @State + internal var results = [DataSource.ID]() + + @State + internal var tasks = [DataSource.ID: Task]() + + @State + internal var errors = [DataSource.ID: DataSource.Failure]() + + public init( + dataSource: DataSource, + configuration: DataSource.Configuration + ) { + self.dataSource = dataSource + self.configuration = configuration + } +} + +public extension AsyncFetchedResults { + + func reset() { + results.removeAll(keepingCapacity: true) + tasks.values.forEach { $0.cancel() } + tasks.removeAll() + } + + func reload() { + reset() + results = dataSource.fetch(configuration: configuration) + } +} + +public extension AsyncFetchedResults { + + enum Element { + case loading(DataSource.ID) + case success(DataSource.ID, DataSource.Success) + case failure(DataSource.ID, DataSource.Failure) + } +} + +extension AsyncFetchedResults.Element: Identifiable { + + public var id: DataSource.ID { + switch self { + case let .loading(id): + return id + case let .success(id, _): + return id + case let .failure(id, _): + return id + } + } +} + +private extension AsyncFetchedResults.Element { + + init( + id: DataSource.ID, + result: Result? + ) { + guard let result = result else { + self = .loading(id) + return + } + switch result { + case .success(let success): + self = .success(id, success) + case .failure(let failure): + self = .failure(id, failure) + } + } +} + +extension AsyncFetchedResults: RandomAccessCollection { + + public var count: Int { + // fetch + results = dataSource.fetch(configuration: configuration) + return results.count + } + + public var startIndex: Int { + results.startIndex + } + + public var endIndex: Int { + results.endIndex + } + + public func index(after index: Int) -> Int { + results.index(after: index) + } + + public subscript(index: Int) -> Element { + let id = results[index] + // return cached value + if let cachedValue = dataSource.cachedValue(for: id) { + return .success(id, cachedValue) + } else { + // async load + if tasks[id] == nil { + tasks[id] = Task { + // load + let result = await dataSource.load(id: id) + // save error + switch result { + case .success: + break // observer should update + case .failure(let failure): + self.errors[id] = failure + } + // remove task so it can be reloaded if not cached + self.tasks[id] = nil + } + } + if let error = errors[id] { + return .failure(id, error) + } else { + assert(tasks[id] != nil) + return .loading(id) + } + } + } +} diff --git a/Xcode/LockKit/Model/NewKeyInvitationStore.swift b/Xcode/LockKit/Model/NewKeyInvitationStore.swift index a4109e2a..2c13e721 100644 --- a/Xcode/LockKit/Model/NewKeyInvitationStore.swift +++ b/Xcode/LockKit/Model/NewKeyInvitationStore.swift @@ -38,8 +38,9 @@ public final class NewKeyInvitationStore: ObservableObject { // wait for task try await writeTask.value await self.cache(invitation, url: fileURL) + return fileURL } - + /* @discardableResult public func fetchAll() async throws -> Cache { let documentsURL = try self.documentsURL @@ -87,7 +88,7 @@ public final class NewKeyInvitationStore: ObservableObject { } return newValue } - + */ internal var documentsURL: URL { get throws { guard let url = fileManager.documentsURL else { diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index e490eac8..05d4b985 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -83,6 +83,7 @@ 6E6A97F128DBEE0700C689F6 /* Predicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97F028DBEE0700C689F6 /* Predicate.swift */; }; 6E6A97F328DC030000C689F6 /* NewKeyInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */; }; 6E8BBFDA28DC2CA000F03735 /* SetupLockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */; }; + 6E8BBFDC28DCC8F300F03735 /* AsyncFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */; }; 6E9E37AB28DAA6D100BE7128 /* KeyDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */; }; 6E9E37AD28DADF2600BE7128 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AC28DADF2600BE7128 /* ActivityView.swift */; }; 6E9E37AF28DAEBE000BE7128 /* ActivityItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AE28DAEBE000BE7128 /* ActivityItem.swift */; }; @@ -195,6 +196,7 @@ 6E6A97F028DBEE0700C689F6 /* Predicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Predicate.swift; sourceTree = ""; }; 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyInvitationView.swift; sourceTree = ""; }; 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupLockView.swift; sourceTree = ""; }; + 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncFetchRequest.swift; sourceTree = ""; }; 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyDetailView.swift; sourceTree = ""; }; 6E9E37AC28DADF2600BE7128 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; 6E9E37AE28DAEBE000BE7128 /* ActivityItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityItem.swift; sourceTree = ""; }; @@ -343,6 +345,7 @@ 6E21834F28D9506100A622B3 /* Preferences.swift */, 6E21831228D80FDD00A622B3 /* JSON.swift */, 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */, + 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */, 6E6A97F028DBEE0700C689F6 /* Predicate.swift */, 6E21830F28D80DCD00A622B3 /* Keychain.swift */, 6E21831628D8116C00A622B3 /* NewKey.swift */, @@ -657,6 +660,7 @@ 6E21835028D9506100A622B3 /* Preferences.swift in Sources */, 6E21831828D8116C00A622B3 /* NewKey.swift in Sources */, 6E21835F28D9516B00A622B3 /* CloudApplicationData.swift in Sources */, + 6E8BBFDC28DCC8F300F03735 /* AsyncFetchRequest.swift in Sources */, 6E21831028D80DCD00A622B3 /* Keychain.swift in Sources */, 6E21833C28D8F3B500A622B3 /* KeyManagedObject.swift in Sources */, 6E21830128D7C37500A622B3 /* Error.swift in Sources */, From 089752a0c82d31fdd8e7482f4d674bd2fdb9f225 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 22 Sep 2022 19:54:51 -0700 Subject: [PATCH 134/229] [App] Added AppIntents --- Sources/CoreLock/Status.swift | 2 +- Sources/CoreLock/UnlockAction.swift | 2 +- Xcode/LockIntents/Info.plist | 11 + Xcode/LockIntents/LockEntity.swift | 90 +++++++ Xcode/LockIntents/LockIntents.entitlements | 32 +++ Xcode/LockIntents/LockIntentsExtension.swift | 14 ++ Xcode/LockIntents/LockQuery.swift | 27 ++ Xcode/LockIntents/ScanLocksIntent.swift | 27 ++ Xcode/LockIntents/UnlockIntent.swift | 8 + Xcode/LockKit/Model/Central.swift | 22 ++ .../Model/CoreData/LockManagedObject.swift | 18 ++ Xcode/LockKit/Model/Store.swift | 149 +++++++---- Xcode/SmartLock.xcodeproj/project.pbxproj | 235 ++++++++++++++++-- .../xcschemes/LockIntents.xcscheme | 95 +++++++ .../xcshareddata/xcschemes/SmartLock.xcscheme | 2 +- Xcode/SmartLock/View/NearbyDevicesView.swift | 2 +- Xcode/SmartLock/View/SidebarView.swift | 15 +- 17 files changed, 661 insertions(+), 90 deletions(-) create mode 100644 Xcode/LockIntents/Info.plist create mode 100644 Xcode/LockIntents/LockEntity.swift create mode 100644 Xcode/LockIntents/LockIntents.entitlements create mode 100644 Xcode/LockIntents/LockIntentsExtension.swift create mode 100644 Xcode/LockIntents/LockQuery.swift create mode 100644 Xcode/LockIntents/ScanLocksIntent.swift create mode 100644 Xcode/LockIntents/UnlockIntent.swift create mode 100644 Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockIntents.xcscheme diff --git a/Sources/CoreLock/Status.swift b/Sources/CoreLock/Status.swift index 4334c686..ee6c6411 100644 --- a/Sources/CoreLock/Status.swift +++ b/Sources/CoreLock/Status.swift @@ -10,7 +10,7 @@ import Foundation import TLVCoding /// Lock status -public enum LockStatus: UInt8, CaseIterable { +public enum LockStatus: UInt8, CaseIterable, Sendable { /// Initial Status case setup = 0x00 diff --git a/Sources/CoreLock/UnlockAction.swift b/Sources/CoreLock/UnlockAction.swift index e88d5144..5bb3d0c5 100644 --- a/Sources/CoreLock/UnlockAction.swift +++ b/Sources/CoreLock/UnlockAction.swift @@ -9,7 +9,7 @@ import Foundation import TLVCoding /// Unlock Action -public enum UnlockAction: UInt8, BitMaskOption { +public enum UnlockAction: UInt8, BitMaskOption, Sendable { /// Unlock immediately. case `default` = 0b01 diff --git a/Xcode/LockIntents/Info.plist b/Xcode/LockIntents/Info.plist new file mode 100644 index 00000000..8d15acbe --- /dev/null +++ b/Xcode/LockIntents/Info.plist @@ -0,0 +1,11 @@ + + + + + EXAppExtensionAttributes + + EXExtensionPointIdentifier + com.apple.appintents-extension + + + diff --git a/Xcode/LockIntents/LockEntity.swift b/Xcode/LockIntents/LockEntity.swift new file mode 100644 index 00000000..9a8b28f7 --- /dev/null +++ b/Xcode/LockIntents/LockEntity.swift @@ -0,0 +1,90 @@ +// +// LockEntity.swift +// LockIntents +// +// Created by Alsey Coleman Miller on 9/22/22. +// + +import AppIntents +import LockKit + +/// Lock Intent Entity +struct LockEntity: AppEntity, Identifiable { + + let id: UUID + + /// Firmware build number + var buildVersion: UInt64 + + /// Firmware version + var version: String + + /// Device state + var status: LockStatus + + /// Supported lock actions + var unlockActions: Set +} + +extension LockEntity { + + static var defaultQuery = LockQuery() + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Lock" + } + + var displayRepresentation: DisplayRepresentation { + let name: LocalizedStringResource + if let key = FileManager.Lock.shared.applicationData?.locks[id] { + name = "\(key.name)" + } else { + name = "Lock" + } + return DisplayRepresentation( + title: name, + subtitle: "UUID \(id.description) v\(version.description)", + image: .init(systemName: "lock.fill") + ) + } +} + +extension LockEntity { + + init(information: LockInformation) { + self.id = information.id + self.buildVersion = information.buildVersion.rawValue + self.version = information.version.rawValue + self.status = information.status + self.unlockActions = information.unlockActions + } +} + +extension LockStatus: AppEnum { + + public static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Lock Status" + } + + public static var caseDisplayRepresentations: [LockStatus : DisplayRepresentation] { + [ + .setup: "Needs Setup", + .unlock: "Ready to Unlock" + ] + } +} + +extension UnlockAction: AppEnum { + + public static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Unlock Action" + } + + public static var caseDisplayRepresentations: [UnlockAction : DisplayRepresentation] { + [ + .default: "Default", + .button: "Button" + ] + } +} + diff --git a/Xcode/LockIntents/LockIntents.entitlements b/Xcode/LockIntents/LockIntents.entitlements new file mode 100644 index 00000000..1fa7223c --- /dev/null +++ b/Xcode/LockIntents/LockIntents.entitlements @@ -0,0 +1,32 @@ + + + + + aps-environment + development + com.apple.developer.aps-environment + development + com.apple.developer.icloud-container-identifiers + + iCloud.com.colemancda.Lock + + com.apple.developer.icloud-services + + CloudKit + + com.apple.developer.ubiquity-kvstore-identifier + $(TeamIdentifierPrefix)$(CFBundleIdentifier) + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.com.colemancda.Lock + + com.apple.security.device.bluetooth + + keychain-access-groups + + $(AppIdentifierPrefix)com.colemancda.Lock + + + diff --git a/Xcode/LockIntents/LockIntentsExtension.swift b/Xcode/LockIntents/LockIntentsExtension.swift new file mode 100644 index 00000000..b1054f77 --- /dev/null +++ b/Xcode/LockIntents/LockIntentsExtension.swift @@ -0,0 +1,14 @@ +// +// LockIntentsExtension.swift +// LockIntents +// +// Created by Alsey Coleman Miller on 9/22/22. +// + +import AppIntents + +@main +struct LockIntentsExtension: AppIntentsExtension { + + +} diff --git a/Xcode/LockIntents/LockQuery.swift b/Xcode/LockIntents/LockQuery.swift new file mode 100644 index 00000000..2f21f388 --- /dev/null +++ b/Xcode/LockIntents/LockQuery.swift @@ -0,0 +1,27 @@ +// +// LockQuery.swift +// LockIntents +// +// Created by Alsey Coleman Miller on 9/22/22. +// + +import AppIntents +import LockKit + +struct LockQuery: EntityQuery { + + func entities(for identifiers: [UUID]) async throws -> [LockEntity] { + let store = await Store.shared + return await store.applicationData.locks + .filter { identifiers.contains($0.key) } + .map { + LockEntity( + id: $0.key, + buildVersion: $0.value.information.buildVersion.rawValue, + version: $0.value.information.version.rawValue, + status: $0.value.information.status, + unlockActions: $0.value.information.unlockActions + ) + } + } +} diff --git a/Xcode/LockIntents/ScanLocksIntent.swift b/Xcode/LockIntents/ScanLocksIntent.swift new file mode 100644 index 00000000..d269cd8d --- /dev/null +++ b/Xcode/LockIntents/ScanLocksIntent.swift @@ -0,0 +1,27 @@ +// +// LockIntents.swift +// LockIntents +// +// Created by Alsey Coleman Miller on 9/22/22. +// + +import AppIntents +import LockKit + +struct ScanLocksIntent: AppIntent { + + static var title: LocalizedStringResource = "Scan for Locks" + + @Parameter(title: "Duration", default: 1) + var duration: TimeInterval + + func perform() async throws -> some IntentResult { + let store = await Store.shared + try await store.scan(duration: duration) + let locks = await store.lockInformation + .lazy + .sorted(by: { $0.key.id.description < $1.key.id.description }) + .map { LockEntity(information: $0.value) } + return .result(value: locks) + } +} diff --git a/Xcode/LockIntents/UnlockIntent.swift b/Xcode/LockIntents/UnlockIntent.swift new file mode 100644 index 00000000..4d32cf05 --- /dev/null +++ b/Xcode/LockIntents/UnlockIntent.swift @@ -0,0 +1,8 @@ +// +// UnlockIntent.swift +// LockIntents +// +// Created by Alsey Coleman Miller on 9/22/22. +// + +import Foundation diff --git a/Xcode/LockKit/Model/Central.swift b/Xcode/LockKit/Model/Central.swift index f4d41c4a..cf0830e4 100644 --- a/Xcode/LockKit/Model/Central.swift +++ b/Xcode/LockKit/Model/Central.swift @@ -14,4 +14,26 @@ import DarwinGATT public typealias NativeCentral = DarwinCentral public typealias NativePeripheral = DarwinCentral.Peripheral + +extension DarwinCentral { + + /// Wait for CoreBluetooth to be ready. + func waitPowerOn(warning: Int = 3, timeout: Int = 10) async throws { + + var powerOnWait = 0 + while await state != .poweredOn { + + // inform user after 3 seconds + if powerOnWait == warning { + print("Waiting for CoreBluetooth to be ready, please turn on Bluetooth") + } + + try await Task.sleep(nanoseconds: 1_000_000_000) + powerOnWait += 1 + guard powerOnWait < timeout + else { throw LockError.bluetoothUnavailable } + } + } +} + #endif diff --git a/Xcode/LockKit/Model/CoreData/LockManagedObject.swift b/Xcode/LockKit/Model/CoreData/LockManagedObject.swift index 4af800da..affe99f9 100644 --- a/Xcode/LockKit/Model/CoreData/LockManagedObject.swift +++ b/Xcode/LockKit/Model/CoreData/LockManagedObject.swift @@ -9,6 +9,7 @@ import Foundation import CoreData import CoreLock +import Predicate public final class LockManagedObject: NSManagedObject { @@ -55,6 +56,23 @@ public extension LockManagedObject { ] return try context.fetch(fetchRequest) } + + static func fetch( + for identifiers: [UUID], + in context: NSManagedObjectContext, + sort: [NSSortDescriptor] = [] + ) throws -> [LockManagedObject] { + let fetchRequest = LockManagedObject.fetchRequest() + let predicate = #keyPath(LockManagedObject.identifier).in(identifiers) + fetchRequest.predicate = predicate.toFoundation() + assert(fetchRequest.predicate?.description == predicate.description) + fetchRequest.fetchBatchSize = identifiers.count + fetchRequest.fetchLimit = identifiers.count + fetchRequest.sortDescriptors = sort.isEmpty == false ? sort : [ + .init(keyPath: \LockManagedObject.identifier, ascending: true) + ] + return try context.fetch(fetchRequest) + } } // MARK: - Store diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index c7cf6ad2..6fd698a7 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -268,7 +268,7 @@ public extension Store { if newState != oldValue { self.state = newState if newState == .poweredOn, isScanning == false { - await self.scan() + self.scanDefault() } } try await Task.sleep(timeInterval: 0.5) @@ -281,14 +281,77 @@ public extension Store { return lockInformation.first(where: { $0.value.id == id })?.key } - func scan() async { - guard await central.state == .poweredOn else { - isScanning = false + func scanDefault() { + guard state == .poweredOn else { return } - isScanning = true - if let stream = scanStream, stream.isScanning { - return // already scanning + Task { + let bluetoothState = await self.central.state + guard bluetoothState == .poweredOn else { + throw LockError.bluetoothUnavailable + } + self.isScanning = true + if let stream = scanStream, stream.isScanning { + return // already scanning + } + self.scanStream = nil + let filterDuplicates = true //preferences.filterDuplicates + self.peripherals.removeAll(keepingCapacity: true) + self.stopScanning() + self.isScanning = true + let stream = central.scan( + with: [LockService.uuid], + filterDuplicates: filterDuplicates + ) + self.scanStream = stream + // process scanned devices + Task { + defer { Task { await MainActor.run { self.isScanning = false } } } + do { + for try await scanData in stream { + guard let serviceUUIDs = scanData.advertisementData.serviceUUIDs, + serviceUUIDs.contains(LockService.uuid) + else { continue } + // cache found device + try? await Task.sleep(timeInterval: 0.6) + self.peripherals[scanData.peripheral] = scanData + } + } catch { + log("⚠️ Unable to scan. \(error)") + } + } + // stop scanning after 5 sec if need to read device info + Task { + let loading = { + self.peripherals + .keys + .filter { !self.lockInformation.keys.contains($0) } + } + try? await Task.sleep(timeInterval: 3) + while self.isScanning, loading().isEmpty { + try? await Task.sleep(timeInterval: 2) + } + // stop scanning and load info for unknown devices + self.stopScanning() + await Task.bluetooth { + for peripheral in loading() { + self.stopScanning() + do { + let _ = try await self.readInformation(for: peripheral) + } catch { + log("⚠️ Unable to load information for peripheral \(peripheral). \(error)") + } + } + } + } + } + } + + func scan(duration: TimeInterval) async throws { + precondition(duration > 0.001) + let bluetoothState = await central.state + guard bluetoothState == .poweredOn else { + throw LockError.bluetoothUnavailable } self.scanStream = nil let filterDuplicates = true //preferences.filterDuplicates @@ -300,57 +363,33 @@ public extension Store { filterDuplicates: filterDuplicates ) self.scanStream = stream - // process scanned devices - Task { + let task = Task { [unowned self] in + defer { Task { await MainActor.run { self.isScanning = false } } } + var peripherals = [NativePeripheral: ScanData]() + for try await scanData in stream { + guard let serviceUUIDs = scanData.advertisementData.serviceUUIDs, + serviceUUIDs.contains(LockService.uuid) + else { continue } + // cache found device + peripherals[scanData.peripheral] = scanData + } + return peripherals + } + try await Task.sleep(timeInterval: duration) + stream.stop() + let peripherals = try await task.value // throw errors + self.peripherals = peripherals + let loading = { + self.peripherals + .keys + .filter { !self.lockInformation.keys.contains($0) } + } + for peripheral in loading() { do { - for try await scanData in stream { - guard let serviceUUIDs = scanData.advertisementData.serviceUUIDs, - serviceUUIDs.contains(LockService.uuid) - else { continue } - // cache found device - try? await Task.sleep(timeInterval: 0.6) - self.peripherals[scanData.peripheral] = scanData - } + let _ = try await self.readInformation(for: peripheral) } catch { - log("⚠️ Unable to scan. \(error)") + log("⚠️ Unable to load information for peripheral \(peripheral). \(error)") } - self.isScanning = false - } - // stop scanning after 5 sec if need to read device info - Task { - let loading = { - self.peripherals - .keys - .filter { !self.lockInformation.keys.contains($0) } - } - try? await Task.sleep(timeInterval: 3) - while self.isScanning, loading().isEmpty { - try? await Task.sleep(timeInterval: 2) - } - // stop scanning and load info for unknown devices - stopScanning() - await Task.bluetooth { - for peripheral in loading() { - self.stopScanning() - do { - let information = try await self.readInformation(for: peripheral) - log("Read information for lock \(information.id)") - #if DEBUG - dump(information) - #endif - } catch { - log("⚠️ Unable to load information for peripheral \(peripheral). \(error)") - } - } - } - } - } - - func scan(duration: TimeInterval) async { - await scan() - Task { - try? await Task.sleep(timeInterval: duration) - stopScanning() } } diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 05d4b985..9dcb790b 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -84,6 +84,13 @@ 6E6A97F328DC030000C689F6 /* NewKeyInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */; }; 6E8BBFDA28DC2CA000F03735 /* SetupLockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */; }; 6E8BBFDC28DCC8F300F03735 /* AsyncFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */; }; + 6E8BBFEB28DD301B00F03735 /* LockIntentsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFEA28DD301B00F03735 /* LockIntentsExtension.swift */; }; + 6E8BBFED28DD301B00F03735 /* ScanLocksIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFEC28DD301B00F03735 /* ScanLocksIntent.swift */; }; + 6E8BBFF128DD301B00F03735 /* LockIntents.appex in Embed ExtensionKit Extensions */ = {isa = PBXBuildFile; fileRef = 6E8BBFE828DD301B00F03735 /* LockIntents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 6E8BBFF628DD30B400F03735 /* LockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; }; + 6E8BBFFC28DD438500F03735 /* LockEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFB28DD438500F03735 /* LockEntity.swift */; }; + 6E8BBFFE28DD491100F03735 /* LockQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFD28DD491100F03735 /* LockQuery.swift */; }; + 6E8BC00028DD492300F03735 /* UnlockIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFF28DD492300F03735 /* UnlockIntent.swift */; }; 6E9E37AB28DAA6D100BE7128 /* KeyDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */; }; 6E9E37AD28DADF2600BE7128 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AC28DADF2600BE7128 /* ActivityView.swift */; }; 6E9E37AF28DAEBE000BE7128 /* ActivityItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AE28DAEBE000BE7128 /* ActivityItem.swift */; }; @@ -98,6 +105,20 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 6E8BBFEF28DD301B00F03735 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6EA7767928D7061600018FA3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6E8BBFE728DD301B00F03735; + remoteInfo = LockIntents; + }; + 6E8BBFF828DD30B400F03735 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6EA7767928D7061600018FA3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6EA7769C28D707FE00018FA3; + remoteInfo = LockKit; + }; 6EA776A128D707FE00018FA3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 6EA7767928D7061600018FA3 /* Project object */; @@ -108,6 +129,17 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 6E8BBFF528DD301B00F03735 /* Embed ExtensionKit Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(EXTENSIONS_FOLDER_PATH)"; + dstSubfolderSpec = 16; + files = ( + 6E8BBFF128DD301B00F03735 /* LockIntents.appex in Embed ExtensionKit Extensions */, + ); + name = "Embed ExtensionKit Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; 6EA776A828D707FE00018FA3 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -197,6 +229,14 @@ 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyInvitationView.swift; sourceTree = ""; }; 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupLockView.swift; sourceTree = ""; }; 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncFetchRequest.swift; sourceTree = ""; }; + 6E8BBFE828DD301B00F03735 /* LockIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.extensionkit-extension"; includeInIndex = 0; path = LockIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 6E8BBFEA28DD301B00F03735 /* LockIntentsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockIntentsExtension.swift; sourceTree = ""; }; + 6E8BBFEC28DD301B00F03735 /* ScanLocksIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanLocksIntent.swift; sourceTree = ""; }; + 6E8BBFEE28DD301B00F03735 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 6E8BBFFB28DD438500F03735 /* LockEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockEntity.swift; sourceTree = ""; }; + 6E8BBFFD28DD491100F03735 /* LockQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockQuery.swift; sourceTree = ""; }; + 6E8BBFFF28DD492300F03735 /* UnlockIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockIntent.swift; sourceTree = ""; }; + 6E8BC00A28DD54E200F03735 /* LockIntents.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LockIntents.entitlements; sourceTree = ""; }; 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyDetailView.swift; sourceTree = ""; }; 6E9E37AC28DADF2600BE7128 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; 6E9E37AE28DAEBE000BE7128 /* ActivityItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityItem.swift; sourceTree = ""; }; @@ -219,6 +259,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6E8BBFE528DD301B00F03735 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E8BBFF628DD30B400F03735 /* LockKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6EA7767E28D7061600018FA3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -400,6 +448,20 @@ path = AppKit; sourceTree = ""; }; + 6E8BBFE928DD301B00F03735 /* LockIntents */ = { + isa = PBXGroup; + children = ( + 6E8BC00A28DD54E200F03735 /* LockIntents.entitlements */, + 6E8BBFEA28DD301B00F03735 /* LockIntentsExtension.swift */, + 6E8BBFEC28DD301B00F03735 /* ScanLocksIntent.swift */, + 6E8BBFFF28DD492300F03735 /* UnlockIntent.swift */, + 6E8BBFFB28DD438500F03735 /* LockEntity.swift */, + 6E8BBFFD28DD491100F03735 /* LockQuery.swift */, + 6E8BBFEE28DD301B00F03735 /* Info.plist */, + ); + path = LockIntents; + sourceTree = ""; + }; 6EA7767828D7061600018FA3 = { isa = PBXGroup; children = ( @@ -407,6 +469,7 @@ 6EA7768328D7061600018FA3 /* SmartLock */, 6EA7769E28D707FE00018FA3 /* LockKit */, 6E3276C428D70A3700AF171B /* MatterLock */, + 6E8BBFE928DD301B00F03735 /* LockIntents */, 6EA7768228D7061600018FA3 /* Products */, 6EA776A928D7082300018FA3 /* Frameworks */, ); @@ -418,6 +481,7 @@ 6EA7768128D7061600018FA3 /* SmartLock.app */, 6EA7769D28D707FE00018FA3 /* LockKit.framework */, 6E3276C128D70A3700AF171B /* MatterLock.appex */, + 6E8BBFE828DD301B00F03735 /* LockIntents.appex */, ); name = Products; sourceTree = ""; @@ -495,6 +559,24 @@ productReference = 6E3276C128D70A3700AF171B /* MatterLock.appex */; productType = "com.apple.product-type.app-extension"; }; + 6E8BBFE728DD301B00F03735 /* LockIntents */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6E8BBFF228DD301B00F03735 /* Build configuration list for PBXNativeTarget "LockIntents" */; + buildPhases = ( + 6E8BBFE428DD301B00F03735 /* Sources */, + 6E8BBFE528DD301B00F03735 /* Frameworks */, + 6E8BBFE628DD301B00F03735 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 6E8BBFF928DD30B400F03735 /* PBXTargetDependency */, + ); + name = LockIntents; + productName = LockIntents; + productReference = 6E8BBFE828DD301B00F03735 /* LockIntents.appex */; + productType = "com.apple.product-type.extensionkit-extension"; + }; 6EA7768028D7061600018FA3 /* SmartLock */ = { isa = PBXNativeTarget; buildConfigurationList = 6EA7769528D7061600018FA3 /* Build configuration list for PBXNativeTarget "SmartLock" */; @@ -503,11 +585,13 @@ 6EA7767E28D7061600018FA3 /* Frameworks */, 6EA7767F28D7061600018FA3 /* Resources */, 6EA776A828D707FE00018FA3 /* Embed Frameworks */, + 6E8BBFF528DD301B00F03735 /* Embed ExtensionKit Extensions */, ); buildRules = ( ); dependencies = ( 6EA776A228D707FE00018FA3 /* PBXTargetDependency */, + 6E8BBFF028DD301B00F03735 /* PBXTargetDependency */, ); name = SmartLock; productName = SmartLock; @@ -548,12 +632,15 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1400; + LastSwiftUpdateCheck = 1410; + LastUpgradeCheck = 1410; TargetAttributes = { 6E3276C028D70A3700AF171B = { CreatedOnToolsVersion = 14.0; }; + 6E8BBFE728DD301B00F03735 = { + CreatedOnToolsVersion = 14.1; + }; 6EA7768028D7061600018FA3 = { CreatedOnToolsVersion = 14.0; }; @@ -586,6 +673,7 @@ 6EA7768028D7061600018FA3 /* SmartLock */, 6EA7769C28D707FE00018FA3 /* LockKit */, 6E3276C028D70A3700AF171B /* MatterLock */, + 6E8BBFE728DD301B00F03735 /* LockIntents */, ); }; /* End PBXProject section */ @@ -598,6 +686,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6E8BBFE628DD301B00F03735 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6EA7767F28D7061600018FA3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -625,6 +720,18 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6E8BBFE428DD301B00F03735 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E8BBFFC28DD438500F03735 /* LockEntity.swift in Sources */, + 6E8BBFFE28DD491100F03735 /* LockQuery.swift in Sources */, + 6E8BC00028DD492300F03735 /* UnlockIntent.swift in Sources */, + 6E8BBFED28DD301B00F03735 /* ScanLocksIntent.swift in Sources */, + 6E8BBFEB28DD301B00F03735 /* LockIntentsExtension.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6EA7767D28D7061600018FA3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -716,6 +823,16 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 6E8BBFF028DD301B00F03735 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6E8BBFE728DD301B00F03735 /* LockIntents */; + targetProxy = 6E8BBFEF28DD301B00F03735 /* PBXContainerItemProxy */; + }; + 6E8BBFF928DD30B400F03735 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6EA7769C28D707FE00018FA3 /* LockKit */; + targetProxy = 6E8BBFF828DD30B400F03735 /* PBXContainerItemProxy */; + }; 6EA776A228D707FE00018FA3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 6EA7769C28D707FE00018FA3 /* LockKit */; @@ -728,19 +845,17 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4W79SG34MW; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = MatterLock/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MatterLock; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock.MatterLock; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -755,19 +870,17 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4W79SG34MW; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = MatterLock/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MatterLock; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock.MatterLock; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -779,6 +892,73 @@ }; name = Release; }; + 6E8BBFF328DD301B00F03735 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; + CODE_SIGN_ENTITLEMENTS = LockIntents/LockIntents.entitlements; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 4W79SG34MW; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LockIntents/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LockIntents; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock.LockIntents; + PRODUCT_NAME = LockIntents; + SDKROOT = auto; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6E8BBFF428DD301B00F03735 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; + CODE_SIGN_ENTITLEMENTS = LockIntents/LockIntents.entitlements; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 4W79SG34MW; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LockIntents/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LockIntents; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock.LockIntents; + PRODUCT_NAME = LockIntents; + SDKROOT = auto; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; 6EA7769328D7061600018FA3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -812,6 +992,8 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 123; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -831,6 +1013,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -874,6 +1057,8 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 123; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -887,6 +1072,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SWIFT_COMPILATION_MODE = wholemodule; @@ -903,8 +1089,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = SmartLock/SmartLock.entitlements; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"SmartLock/Preview Content\""; DEVELOPMENT_TEAM = 4W79SG34MW; ENABLE_HARDENED_RUNTIME = YES; @@ -926,13 +1115,12 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; - MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -946,8 +1134,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = SmartLock/SmartLock.entitlements; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"SmartLock/Preview Content\""; DEVELOPMENT_TEAM = 4W79SG34MW; ENABLE_HARDENED_RUNTIME = YES; @@ -969,13 +1160,12 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; - MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -985,9 +1175,10 @@ 6EA776A628D707FE00018FA3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 4W79SG34MW; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1004,7 +1195,6 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.LockKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = auto; @@ -1022,9 +1212,10 @@ 6EA776A728D707FE00018FA3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 4W79SG34MW; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1041,7 +1232,6 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.LockKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = auto; @@ -1067,6 +1257,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 6E8BBFF228DD301B00F03735 /* Build configuration list for PBXNativeTarget "LockIntents" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6E8BBFF328DD301B00F03735 /* Debug */, + 6E8BBFF428DD301B00F03735 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 6EA7767C28D7061600018FA3 /* Build configuration list for PBXProject "SmartLock" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockIntents.xcscheme b/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockIntents.xcscheme new file mode 100644 index 00000000..ac98ff1b --- /dev/null +++ b/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockIntents.xcscheme @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/SmartLock.xcscheme b/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/SmartLock.xcscheme index a8c47418..23825955 100644 --- a/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/SmartLock.xcscheme +++ b/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/SmartLock.xcscheme @@ -1,6 +1,6 @@ Date: Fri, 23 Sep 2022 01:43:02 -0700 Subject: [PATCH 135/229] [App] Fixed `ScanLocksIntent` --- Xcode/LockIntents/LockEntity.swift | 34 ++++--- Xcode/LockIntents/LockIntentsExtension.swift | 1 - Xcode/LockIntents/LockQuery.swift | 4 +- Xcode/LockIntents/ScanLocksIntent.swift | 8 +- Xcode/LockIntents/Shortcuts.swift | 21 +++++ Xcode/LockIntents/TestIntent.swift | 89 +++++++++++++++++++ Xcode/LockKit/Log.swift | 2 +- Xcode/LockKit/Model/Central.swift | 4 +- Xcode/LockKit/Model/Store.swift | 3 +- Xcode/SmartLock.xcodeproj/project.pbxproj | 20 ++++- .../xcschemes/LockIntents.xcscheme | 11 ++- Xcode/SmartLock/View/NearbyDevicesView.swift | 2 +- Xcode/SmartLock/View/TabBarView.swift | 6 +- 13 files changed, 179 insertions(+), 26 deletions(-) create mode 100644 Xcode/LockIntents/Shortcuts.swift create mode 100644 Xcode/LockIntents/TestIntent.swift diff --git a/Xcode/LockIntents/LockEntity.swift b/Xcode/LockIntents/LockEntity.swift index 9a8b28f7..4102770b 100644 --- a/Xcode/LockIntents/LockEntity.swift +++ b/Xcode/LockIntents/LockEntity.swift @@ -35,14 +35,15 @@ extension LockEntity { } var displayRepresentation: DisplayRepresentation { + /* let name: LocalizedStringResource if let key = FileManager.Lock.shared.applicationData?.locks[id] { name = "\(key.name)" } else { name = "Lock" - } + }*/ return DisplayRepresentation( - title: name, + title: "Lock", subtitle: "UUID \(id.description) v\(version.description)", image: .init(systemName: "lock.fill") ) @@ -55,18 +56,24 @@ extension LockEntity { self.id = information.id self.buildVersion = information.buildVersion.rawValue self.version = information.version.rawValue - self.status = information.status - self.unlockActions = information.unlockActions + self.status = .init(rawValue: information.status.rawValue)! + self.unlockActions = .init(information.unlockActions.map { .init(rawValue: $0.rawValue)! }) } } -extension LockStatus: AppEnum { +enum LockStatus: UInt8, AppEnum { - public static var typeDisplayRepresentation: TypeDisplayRepresentation { + /// Initial Status + case setup = 0x00 + + /// Idle / Unlock Mode + case unlock = 0x01 + + static var typeDisplayRepresentation: TypeDisplayRepresentation { "Lock Status" } - public static var caseDisplayRepresentations: [LockStatus : DisplayRepresentation] { + static var caseDisplayRepresentations: [LockStatus : DisplayRepresentation] { [ .setup: "Needs Setup", .unlock: "Ready to Unlock" @@ -74,17 +81,22 @@ extension LockStatus: AppEnum { } } -extension UnlockAction: AppEnum { +enum UnlockAction: UInt8, AppEnum { + + /// Unlock immediately. + case `default` = 0b01 - public static var typeDisplayRepresentation: TypeDisplayRepresentation { + /// Unlock when button is pressed. + case button = 0b10 + + static var typeDisplayRepresentation: TypeDisplayRepresentation { "Unlock Action" } - public static var caseDisplayRepresentations: [UnlockAction : DisplayRepresentation] { + static var caseDisplayRepresentations: [UnlockAction : DisplayRepresentation] { [ .default: "Default", .button: "Button" ] } } - diff --git a/Xcode/LockIntents/LockIntentsExtension.swift b/Xcode/LockIntents/LockIntentsExtension.swift index b1054f77..f3dac016 100644 --- a/Xcode/LockIntents/LockIntentsExtension.swift +++ b/Xcode/LockIntents/LockIntentsExtension.swift @@ -10,5 +10,4 @@ import AppIntents @main struct LockIntentsExtension: AppIntentsExtension { - } diff --git a/Xcode/LockIntents/LockQuery.swift b/Xcode/LockIntents/LockQuery.swift index 2f21f388..05ef8e34 100644 --- a/Xcode/LockIntents/LockQuery.swift +++ b/Xcode/LockIntents/LockQuery.swift @@ -19,8 +19,8 @@ struct LockQuery: EntityQuery { id: $0.key, buildVersion: $0.value.information.buildVersion.rawValue, version: $0.value.information.version.rawValue, - status: $0.value.information.status, - unlockActions: $0.value.information.unlockActions + status: .init(rawValue: $0.value.information.status.rawValue)!, + unlockActions: .init($0.value.information.unlockActions.map { .init(rawValue: $0.rawValue)! }) ) } } diff --git a/Xcode/LockIntents/ScanLocksIntent.swift b/Xcode/LockIntents/ScanLocksIntent.swift index d269cd8d..ebebeeb5 100644 --- a/Xcode/LockIntents/ScanLocksIntent.swift +++ b/Xcode/LockIntents/ScanLocksIntent.swift @@ -17,11 +17,17 @@ struct ScanLocksIntent: AppIntent { func perform() async throws -> some IntentResult { let store = await Store.shared + do { try await store.central.waitPowerOn() } + catch { + return .result(value: [LockEntity]()) + } try await store.scan(duration: duration) let locks = await store.lockInformation .lazy .sorted(by: { $0.key.id.description < $1.key.id.description }) .map { LockEntity(information: $0.value) } - return .result(value: locks) + return .result( + value: locks + ) } } diff --git a/Xcode/LockIntents/Shortcuts.swift b/Xcode/LockIntents/Shortcuts.swift new file mode 100644 index 00000000..3f12d138 --- /dev/null +++ b/Xcode/LockIntents/Shortcuts.swift @@ -0,0 +1,21 @@ +// +// Shortcuts.swift +// LockIntents +// +// Created by Alsey Coleman Miller on 9/22/22. +// + +import AppIntents + +struct AppShortcuts: AppShortcutsProvider { + + static var appShortcuts: [AppShortcut] { + AppShortcut( + intent: ScanLocksIntent(), + phrases: [ + "Scan for locks with \(.applicationName)", + ], + systemImageName: "lock" + ) + } +} diff --git a/Xcode/LockIntents/TestIntent.swift b/Xcode/LockIntents/TestIntent.swift new file mode 100644 index 00000000..faedb6d9 --- /dev/null +++ b/Xcode/LockIntents/TestIntent.swift @@ -0,0 +1,89 @@ +// +// TestIntent.swift +// LockIntents +// +// Created by Alsey Coleman Miller on 9/23/22. +// + +import AppIntents + +#if DEBUG0 +struct TestIntents: AppIntent { + + static var title: LocalizedStringResource = "TestIntents" + + @Parameter(title: "Duration", default: 2) + var duration: TimeInterval + + func perform() async throws -> some IntentResult { + try await Task.sleep(for: .seconds(duration)) + let entity = { TestLockEntity(id: UUID(), buildVersion: 1, version: "1.0.0") } + return .result(value: [entity(), entity(), entity()]) + } +} + +struct TestIntents2: AppIntent { + + static var title: LocalizedStringResource = "TestIntents 2" + + @Parameter(title: "Lock") + var lock: TestLockEntity + + func perform() async throws -> some IntentResult { + try await Task.sleep(for: .seconds(1)) + let entity = { TestLockEntity(id: UUID(), buildVersion: 1, version: "1.0.0") } + return .result(value: entity().id.description) + } +} + + +/// Lock Intent Entity +struct TestLockEntity: AppEntity, Identifiable, Sendable { + + let id: UUID + + /// Firmware build number + var buildVersion: UInt64 + + /// Firmware version + var version: String + + /// Device state + //var status: CoreLock.LockStatus + + /// Supported lock actions + //var unlockActions: Set +} + +extension TestLockEntity { + + static var defaultQuery = TestLockQuery() + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Lock" + } + + var displayRepresentation: DisplayRepresentation { + DisplayRepresentation( + title: "Lock", + subtitle: "UUID \(id.description) v\(version.description)", + image: .init(systemName: "lock.fill") + ) + } + + func suggestedEntities() async throws -> [TestLockEntity] { + let entity = { TestLockEntity(id: UUID(), buildVersion: 1, version: "1.0.0") } + return [entity(), entity(), entity()] + } +} + +struct TestLockQuery: EntityQuery { + + func entities(for identifiers: [UUID]) async throws -> [TestLockEntity] { + return identifiers.map { + TestLockEntity(id: $0, buildVersion: 1, version: "1") + } + } +} + +#endif diff --git a/Xcode/LockKit/Log.swift b/Xcode/LockKit/Log.swift index 70638962..426f6e5a 100644 --- a/Xcode/LockKit/Log.swift +++ b/Xcode/LockKit/Log.swift @@ -9,6 +9,6 @@ import Foundation public func log(_ message: String) { DispatchQueue.main.async { - print(message) + NSLog(message) } } diff --git a/Xcode/LockKit/Model/Central.swift b/Xcode/LockKit/Model/Central.swift index cf0830e4..942e99a9 100644 --- a/Xcode/LockKit/Model/Central.swift +++ b/Xcode/LockKit/Model/Central.swift @@ -15,7 +15,7 @@ import DarwinGATT public typealias NativeCentral = DarwinCentral public typealias NativePeripheral = DarwinCentral.Peripheral -extension DarwinCentral { +public extension DarwinCentral { /// Wait for CoreBluetooth to be ready. func waitPowerOn(warning: Int = 3, timeout: Int = 10) async throws { @@ -25,7 +25,7 @@ extension DarwinCentral { // inform user after 3 seconds if powerOnWait == warning { - print("Waiting for CoreBluetooth to be ready, please turn on Bluetooth") + NSLog("Waiting for CoreBluetooth to be ready, please turn on Bluetooth") } try await Task.sleep(nanoseconds: 1_000_000_000) diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 6fd698a7..5050d366 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -268,7 +268,7 @@ public extension Store { if newState != oldValue { self.state = newState if newState == .poweredOn, isScanning == false { - self.scanDefault() + //self.scanDefault() } } try await Task.sleep(timeInterval: 0.5) @@ -384,6 +384,7 @@ public extension Store { .keys .filter { !self.lockInformation.keys.contains($0) } } + NSLog("LOG: \(self.peripherals)") for peripheral in loading() { do { let _ = try await self.readInformation(for: peripheral) diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 9dcb790b..56083ca2 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -82,14 +82,16 @@ 6E6A97EF28DBEC2D00C689F6 /* NewKeyInvitationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */; }; 6E6A97F128DBEE0700C689F6 /* Predicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97F028DBEE0700C689F6 /* Predicate.swift */; }; 6E6A97F328DC030000C689F6 /* NewKeyInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */; }; + 6E84E52028DD9646008CAE85 /* Shortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E51F28DD9646008CAE85 /* Shortcuts.swift */; }; + 6E84E52228DD9713008CAE85 /* TestIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E52128DD9713008CAE85 /* TestIntent.swift */; }; + 6E84E52328DD97D1008CAE85 /* LockQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFD28DD491100F03735 /* LockQuery.swift */; }; + 6E84E52428DD97D5008CAE85 /* LockEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFB28DD438500F03735 /* LockEntity.swift */; }; 6E8BBFDA28DC2CA000F03735 /* SetupLockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */; }; 6E8BBFDC28DCC8F300F03735 /* AsyncFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */; }; 6E8BBFEB28DD301B00F03735 /* LockIntentsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFEA28DD301B00F03735 /* LockIntentsExtension.swift */; }; 6E8BBFED28DD301B00F03735 /* ScanLocksIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFEC28DD301B00F03735 /* ScanLocksIntent.swift */; }; 6E8BBFF128DD301B00F03735 /* LockIntents.appex in Embed ExtensionKit Extensions */ = {isa = PBXBuildFile; fileRef = 6E8BBFE828DD301B00F03735 /* LockIntents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 6E8BBFF628DD30B400F03735 /* LockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; }; - 6E8BBFFC28DD438500F03735 /* LockEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFB28DD438500F03735 /* LockEntity.swift */; }; - 6E8BBFFE28DD491100F03735 /* LockQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFD28DD491100F03735 /* LockQuery.swift */; }; 6E8BC00028DD492300F03735 /* UnlockIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFF28DD492300F03735 /* UnlockIntent.swift */; }; 6E9E37AB28DAA6D100BE7128 /* KeyDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */; }; 6E9E37AD28DADF2600BE7128 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AC28DADF2600BE7128 /* ActivityView.swift */; }; @@ -227,6 +229,8 @@ 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyInvitationStore.swift; sourceTree = ""; }; 6E6A97F028DBEE0700C689F6 /* Predicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Predicate.swift; sourceTree = ""; }; 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyInvitationView.swift; sourceTree = ""; }; + 6E84E51F28DD9646008CAE85 /* Shortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shortcuts.swift; sourceTree = ""; }; + 6E84E52128DD9713008CAE85 /* TestIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestIntent.swift; sourceTree = ""; }; 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupLockView.swift; sourceTree = ""; }; 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncFetchRequest.swift; sourceTree = ""; }; 6E8BBFE828DD301B00F03735 /* LockIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.extensionkit-extension"; includeInIndex = 0; path = LockIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -454,9 +458,11 @@ 6E8BC00A28DD54E200F03735 /* LockIntents.entitlements */, 6E8BBFEA28DD301B00F03735 /* LockIntentsExtension.swift */, 6E8BBFEC28DD301B00F03735 /* ScanLocksIntent.swift */, + 6E84E51F28DD9646008CAE85 /* Shortcuts.swift */, 6E8BBFFF28DD492300F03735 /* UnlockIntent.swift */, 6E8BBFFB28DD438500F03735 /* LockEntity.swift */, 6E8BBFFD28DD491100F03735 /* LockQuery.swift */, + 6E84E52128DD9713008CAE85 /* TestIntent.swift */, 6E8BBFEE28DD301B00F03735 /* Info.plist */, ); path = LockIntents; @@ -724,11 +730,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 6E8BBFFC28DD438500F03735 /* LockEntity.swift in Sources */, - 6E8BBFFE28DD491100F03735 /* LockQuery.swift in Sources */, + 6E84E52428DD97D5008CAE85 /* LockEntity.swift in Sources */, + 6E84E52228DD9713008CAE85 /* TestIntent.swift in Sources */, + 6E84E52328DD97D1008CAE85 /* LockQuery.swift in Sources */, 6E8BC00028DD492300F03735 /* UnlockIntent.swift in Sources */, 6E8BBFED28DD301B00F03735 /* ScanLocksIntent.swift in Sources */, 6E8BBFEB28DD301B00F03735 /* LockIntentsExtension.swift in Sources */, + 6E84E52028DD9646008CAE85 /* Shortcuts.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -912,6 +920,8 @@ "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", + "@executable_path/../../../Frameworks", + "@executable_path/../../../../Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock.LockIntents; PRODUCT_NAME = LockIntents; @@ -945,6 +955,8 @@ "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", + "@executable_path/../../../Frameworks", + "@executable_path/../../../../Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock.LockIntents; PRODUCT_NAME = LockIntents; diff --git a/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockIntents.xcscheme b/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockIntents.xcscheme index ac98ff1b..1025fa5c 100644 --- a/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockIntents.xcscheme +++ b/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockIntents.xcscheme @@ -57,6 +57,15 @@ allowLocationSimulation = "YES"> + + + + - + Date: Fri, 23 Sep 2022 03:06:09 -0700 Subject: [PATCH 136/229] [App] Added view to `ScanLocksIntent` --- Sources/CoreLock/LockInformation.swift | 2 +- Xcode/LockIntents/LockEntity.swift | 10 +--- Xcode/LockIntents/ScanLocksIntent.swift | 78 ++++++++++++++++++++++--- Xcode/LockKit/Model/Store.swift | 2 - 4 files changed, 72 insertions(+), 20 deletions(-) diff --git a/Sources/CoreLock/LockInformation.swift b/Sources/CoreLock/LockInformation.swift index 752ec865..bc7e097d 100644 --- a/Sources/CoreLock/LockInformation.swift +++ b/Sources/CoreLock/LockInformation.swift @@ -7,7 +7,7 @@ import Foundation -public struct LockInformation: Equatable, Hashable, Codable { +public struct LockInformation: Equatable, Hashable, Codable, Identifiable { /// Lock identifier public let id: UUID diff --git a/Xcode/LockIntents/LockEntity.swift b/Xcode/LockIntents/LockEntity.swift index 4102770b..d81207f8 100644 --- a/Xcode/LockIntents/LockEntity.swift +++ b/Xcode/LockIntents/LockEntity.swift @@ -35,17 +35,9 @@ extension LockEntity { } var displayRepresentation: DisplayRepresentation { - /* - let name: LocalizedStringResource - if let key = FileManager.Lock.shared.applicationData?.locks[id] { - name = "\(key.name)" - } else { - name = "Lock" - }*/ return DisplayRepresentation( title: "Lock", - subtitle: "UUID \(id.description) v\(version.description)", - image: .init(systemName: "lock.fill") + subtitle: "\(id.description)" ) } } diff --git a/Xcode/LockIntents/ScanLocksIntent.swift b/Xcode/LockIntents/ScanLocksIntent.swift index ebebeeb5..7bba3628 100644 --- a/Xcode/LockIntents/ScanLocksIntent.swift +++ b/Xcode/LockIntents/ScanLocksIntent.swift @@ -6,28 +6,90 @@ // import AppIntents +import SwiftUI import LockKit struct ScanLocksIntent: AppIntent { + + static var title: LocalizedStringResource { "Scan for Locks" } - static var title: LocalizedStringResource = "Scan for Locks" + static var description: IntentDescription { + IntentDescription( + "Scan for nearby locks", + categoryName: "Utility", + searchKeywords: ["scan", "bluetooth", "lock"] + ) + } - @Parameter(title: "Duration", default: 1) + static var parameterSummary: some ParameterSummary { + Summary("Scan nearby locks for \(\.$duration) seconds") + } + + @Parameter( + title: "Duration", + description: "Duration in seconds for scanning.", + default: 1 + ) var duration: TimeInterval + @MainActor func perform() async throws -> some IntentResult { - let store = await Store.shared + let store = Store.shared do { try await store.central.waitPowerOn() } catch { - return .result(value: [LockEntity]()) + return .result( + value: [LockEntity](), + view: view(for: []) + ) } try await store.scan(duration: duration) - let locks = await store.lockInformation - .lazy + let locks = store.lockInformation .sorted(by: { $0.key.id.description < $1.key.id.description }) - .map { LockEntity(information: $0.value) } + .map { $0.value } return .result( - value: locks + value: locks.map { LockEntity(information: $0) }, + view: view(for: locks) ) } } + +@MainActor +private extension ScanLocksIntent { + + func view(for results: [LockInformation]) -> some View { + VStack(alignment: .leading, spacing: 8) { + if results.isEmpty { + Text("No locks found.") + } else { + ForEach(results) { + view(for: $0) + } + } + } + } + + func view(for lock: LockInformation) -> some View { + /* + let cache = Store.shared.applicationData.locks[lock.id] + return VStack { + Text(verbatim: cache?.name ?? (lock.status == .setup ? "Setup" : "Lock")) + Text(verbatim: lock.id.description) + if let permission = cache?.key.permission.type { + Text(verbatim: permission.localizedText) + } + }*/ + if let cache = Store.shared.applicationData.locks[lock.id] { + return LockRowView( + image: .emoji("🔒"), //.permission(cache.key.permission.type), + title: cache.name, + subtitle: cache.key.permission.type.localizedText + ) + } else { + return LockRowView( + image: .emoji("🔒"), + title: lock.status == .setup ? "Setup" : "Lock", + subtitle: lock.id.description + ) + } + } +} diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 5050d366..9a0ad9a5 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -384,7 +384,6 @@ public extension Store { .keys .filter { !self.lockInformation.keys.contains($0) } } - NSLog("LOG: \(self.peripherals)") for peripheral in loading() { do { let _ = try await self.readInformation(for: peripheral) @@ -687,7 +686,6 @@ public extension Store { #keyPath(KeyManagedObject.identifier) ).description == predicate.description) assert(predicate.description == predicate.toFoundation().description) - print(predicate.toFoundation().description) // fetch let invalidKeys = try context.fetch(fetchRequest) // remove keys from CoreData From fec3d157e09366526229575a6ff0de13c6350b68 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 23 Sep 2022 03:54:19 -0700 Subject: [PATCH 137/229] [App] Added `UnlockIntent` --- Xcode/LockIntents/ScanLocksIntent.swift | 15 +++--- Xcode/LockIntents/UnlockIntent.swift | 63 ++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/Xcode/LockIntents/ScanLocksIntent.swift b/Xcode/LockIntents/ScanLocksIntent.swift index 7bba3628..eaa647b5 100644 --- a/Xcode/LockIntents/ScanLocksIntent.swift +++ b/Xcode/LockIntents/ScanLocksIntent.swift @@ -57,14 +57,17 @@ struct ScanLocksIntent: AppIntent { private extension ScanLocksIntent { func view(for results: [LockInformation]) -> some View { - VStack(alignment: .leading, spacing: 8) { - if results.isEmpty { - Text("No locks found.") - } else { - ForEach(results) { - view(for: $0) + HStack(alignment: .top, spacing: 0) { + VStack(alignment: .leading, spacing: 8) { + if results.isEmpty { + Text("No locks found.") + } else { + ForEach(results) { + view(for: $0) + } } } + Spacer(minLength: 0) } } diff --git a/Xcode/LockIntents/UnlockIntent.swift b/Xcode/LockIntents/UnlockIntent.swift index 4d32cf05..76c6aa6e 100644 --- a/Xcode/LockIntents/UnlockIntent.swift +++ b/Xcode/LockIntents/UnlockIntent.swift @@ -5,4 +5,65 @@ // Created by Alsey Coleman Miller on 9/22/22. // -import Foundation +import AppIntents +import SwiftUI +import LockKit + +struct UnlockIntent: AppIntent { + + static var title: LocalizedStringResource { "Unlock" } + + static var description: IntentDescription { + IntentDescription( + "Unlock a door.", + categoryName: "Utility", + searchKeywords: ["unlock", "bluetooth", "lock"] + ) + } + + static var parameterSummary: some ParameterSummary { + Summary("Unlocks \(\.$lock)") + } + + @Parameter( + title: "Lock", + description: "The specified lock to unlock." + ) + var lock: LockEntity + + @MainActor + func perform() async throws -> some IntentResult { + do { + try await Store.shared.unlock(for: lock.id) + } + catch { + return .result( + value: false, + content: { ResultView(error: error.localizedDescription) } + ) + } + return .result( + value: true, + content: { ResultView(error: nil) } + ) + } +} + +extension UnlockIntent { + + struct ResultView: View { + + let error: String? + + var body: some View { + VStack(alignment: .center, spacing: 8) { + if let error = error { + Text("Unable to unlock") + Text(verbatim: error) + } else { + Text("Unlocked") + } + } + } + } +} From 81081f28ffddc5c40b9c1f7eaa175a5312dfe353 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 23 Sep 2022 04:08:10 -0700 Subject: [PATCH 138/229] [App] Disabled AppIntents extension --- Xcode/LockIntents/KeyEntity.swift | 8 +++ Xcode/LockIntents/LockEntity.swift | 76 +++++++++++++---------- Xcode/LockIntents/LockQuery.swift | 1 + Xcode/LockIntents/ScanLocksIntent.swift | 11 +--- Xcode/LockIntents/Shortcuts.swift | 1 + Xcode/LockIntents/UnlockIntent.swift | 3 + Xcode/SmartLock.xcodeproj/project.pbxproj | 54 +++++----------- 7 files changed, 73 insertions(+), 81 deletions(-) create mode 100644 Xcode/LockIntents/KeyEntity.swift diff --git a/Xcode/LockIntents/KeyEntity.swift b/Xcode/LockIntents/KeyEntity.swift new file mode 100644 index 00000000..1468fdf9 --- /dev/null +++ b/Xcode/LockIntents/KeyEntity.swift @@ -0,0 +1,8 @@ +// +// KeyEntity.swift +// LockIntents +// +// Created by Alsey Coleman Miller on 9/23/22. +// + +import Foundation diff --git a/Xcode/LockIntents/LockEntity.swift b/Xcode/LockIntents/LockEntity.swift index d81207f8..6c57bfd3 100644 --- a/Xcode/LockIntents/LockEntity.swift +++ b/Xcode/LockIntents/LockEntity.swift @@ -9,6 +9,7 @@ import AppIntents import LockKit /// Lock Intent Entity +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) struct LockEntity: AppEntity, Identifiable { let id: UUID @@ -26,6 +27,7 @@ struct LockEntity: AppEntity, Identifiable { var unlockActions: Set } +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) extension LockEntity { static var defaultQuery = LockQuery() @@ -42,6 +44,7 @@ extension LockEntity { } } +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) extension LockEntity { init(information: LockInformation) { @@ -53,42 +56,47 @@ extension LockEntity { } } -enum LockStatus: UInt8, AppEnum { - - /// Initial Status - case setup = 0x00 - - /// Idle / Unlock Mode - case unlock = 0x01 - - static var typeDisplayRepresentation: TypeDisplayRepresentation { - "Lock Status" - } +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +extension LockEntity { - static var caseDisplayRepresentations: [LockStatus : DisplayRepresentation] { - [ - .setup: "Needs Setup", - .unlock: "Ready to Unlock" - ] + enum LockStatus: UInt8, AppEnum { + + /// Initial Status + case setup = 0x00 + + /// Idle / Unlock Mode + case unlock = 0x01 + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Lock Status" + } + + static var caseDisplayRepresentations: [LockStatus : DisplayRepresentation] { + [ + .setup: "Needs Setup", + .unlock: "Ready to Unlock" + ] + } } -} -enum UnlockAction: UInt8, AppEnum { - - /// Unlock immediately. - case `default` = 0b01 - - /// Unlock when button is pressed. - case button = 0b10 - - static var typeDisplayRepresentation: TypeDisplayRepresentation { - "Unlock Action" - } - - static var caseDisplayRepresentations: [UnlockAction : DisplayRepresentation] { - [ - .default: "Default", - .button: "Button" - ] + @available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) + enum UnlockAction: UInt8, AppEnum { + + /// Unlock immediately. + case `default` = 0b01 + + /// Unlock when button is pressed. + case button = 0b10 + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Unlock Action" + } + + static var caseDisplayRepresentations: [UnlockAction : DisplayRepresentation] { + [ + .default: "Default", + .button: "Button" + ] + } } } diff --git a/Xcode/LockIntents/LockQuery.swift b/Xcode/LockIntents/LockQuery.swift index 05ef8e34..9a5d873f 100644 --- a/Xcode/LockIntents/LockQuery.swift +++ b/Xcode/LockIntents/LockQuery.swift @@ -8,6 +8,7 @@ import AppIntents import LockKit +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) struct LockQuery: EntityQuery { func entities(for identifiers: [UUID]) async throws -> [LockEntity] { diff --git a/Xcode/LockIntents/ScanLocksIntent.swift b/Xcode/LockIntents/ScanLocksIntent.swift index eaa647b5..75da876d 100644 --- a/Xcode/LockIntents/ScanLocksIntent.swift +++ b/Xcode/LockIntents/ScanLocksIntent.swift @@ -9,6 +9,7 @@ import AppIntents import SwiftUI import LockKit +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) struct ScanLocksIntent: AppIntent { static var title: LocalizedStringResource { "Scan for Locks" } @@ -53,6 +54,7 @@ struct ScanLocksIntent: AppIntent { } } +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) @MainActor private extension ScanLocksIntent { @@ -72,15 +74,6 @@ private extension ScanLocksIntent { } func view(for lock: LockInformation) -> some View { - /* - let cache = Store.shared.applicationData.locks[lock.id] - return VStack { - Text(verbatim: cache?.name ?? (lock.status == .setup ? "Setup" : "Lock")) - Text(verbatim: lock.id.description) - if let permission = cache?.key.permission.type { - Text(verbatim: permission.localizedText) - } - }*/ if let cache = Store.shared.applicationData.locks[lock.id] { return LockRowView( image: .emoji("🔒"), //.permission(cache.key.permission.type), diff --git a/Xcode/LockIntents/Shortcuts.swift b/Xcode/LockIntents/Shortcuts.swift index 3f12d138..6bb2d6b8 100644 --- a/Xcode/LockIntents/Shortcuts.swift +++ b/Xcode/LockIntents/Shortcuts.swift @@ -7,6 +7,7 @@ import AppIntents +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) struct AppShortcuts: AppShortcutsProvider { static var appShortcuts: [AppShortcut] { diff --git a/Xcode/LockIntents/UnlockIntent.swift b/Xcode/LockIntents/UnlockIntent.swift index 76c6aa6e..22dff073 100644 --- a/Xcode/LockIntents/UnlockIntent.swift +++ b/Xcode/LockIntents/UnlockIntent.swift @@ -9,6 +9,7 @@ import AppIntents import SwiftUI import LockKit +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) struct UnlockIntent: AppIntent { static var title: LocalizedStringResource { "Unlock" } @@ -49,6 +50,8 @@ struct UnlockIntent: AppIntent { } } + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) extension UnlockIntent { struct ResultView: View { diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 56083ca2..e4ea8d39 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -82,17 +82,17 @@ 6E6A97EF28DBEC2D00C689F6 /* NewKeyInvitationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */; }; 6E6A97F128DBEE0700C689F6 /* Predicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97F028DBEE0700C689F6 /* Predicate.swift */; }; 6E6A97F328DC030000C689F6 /* NewKeyInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */; }; - 6E84E52028DD9646008CAE85 /* Shortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E51F28DD9646008CAE85 /* Shortcuts.swift */; }; - 6E84E52228DD9713008CAE85 /* TestIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E52128DD9713008CAE85 /* TestIntent.swift */; }; - 6E84E52328DD97D1008CAE85 /* LockQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFD28DD491100F03735 /* LockQuery.swift */; }; - 6E84E52428DD97D5008CAE85 /* LockEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFB28DD438500F03735 /* LockEntity.swift */; }; + 6E84E52728DDC841008CAE85 /* ScanLocksIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFEC28DD301B00F03735 /* ScanLocksIntent.swift */; }; + 6E84E52828DDC841008CAE85 /* UnlockIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFF28DD492300F03735 /* UnlockIntent.swift */; }; + 6E84E52A28DDC841008CAE85 /* Shortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E51F28DD9646008CAE85 /* Shortcuts.swift */; }; + 6E84E52B28DDC841008CAE85 /* KeyEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E52528DDB0F2008CAE85 /* KeyEntity.swift */; }; + 6E84E52C28DDC841008CAE85 /* LockQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFD28DD491100F03735 /* LockQuery.swift */; }; + 6E84E52D28DDC841008CAE85 /* LockEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFB28DD438500F03735 /* LockEntity.swift */; }; + 6E84E52E28DDC90B008CAE85 /* TestIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E52128DD9713008CAE85 /* TestIntent.swift */; }; 6E8BBFDA28DC2CA000F03735 /* SetupLockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */; }; 6E8BBFDC28DCC8F300F03735 /* AsyncFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */; }; 6E8BBFEB28DD301B00F03735 /* LockIntentsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFEA28DD301B00F03735 /* LockIntentsExtension.swift */; }; - 6E8BBFED28DD301B00F03735 /* ScanLocksIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFEC28DD301B00F03735 /* ScanLocksIntent.swift */; }; - 6E8BBFF128DD301B00F03735 /* LockIntents.appex in Embed ExtensionKit Extensions */ = {isa = PBXBuildFile; fileRef = 6E8BBFE828DD301B00F03735 /* LockIntents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 6E8BBFF628DD30B400F03735 /* LockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; }; - 6E8BC00028DD492300F03735 /* UnlockIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFF28DD492300F03735 /* UnlockIntent.swift */; }; 6E9E37AB28DAA6D100BE7128 /* KeyDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */; }; 6E9E37AD28DADF2600BE7128 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AC28DADF2600BE7128 /* ActivityView.swift */; }; 6E9E37AF28DAEBE000BE7128 /* ActivityItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9E37AE28DAEBE000BE7128 /* ActivityItem.swift */; }; @@ -107,13 +107,6 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 6E8BBFEF28DD301B00F03735 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 6EA7767928D7061600018FA3 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 6E8BBFE728DD301B00F03735; - remoteInfo = LockIntents; - }; 6E8BBFF828DD30B400F03735 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 6EA7767928D7061600018FA3 /* Project object */; @@ -131,17 +124,6 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ - 6E8BBFF528DD301B00F03735 /* Embed ExtensionKit Extensions */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = "$(EXTENSIONS_FOLDER_PATH)"; - dstSubfolderSpec = 16; - files = ( - 6E8BBFF128DD301B00F03735 /* LockIntents.appex in Embed ExtensionKit Extensions */, - ); - name = "Embed ExtensionKit Extensions"; - runOnlyForDeploymentPostprocessing = 0; - }; 6EA776A828D707FE00018FA3 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -231,6 +213,7 @@ 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyInvitationView.swift; sourceTree = ""; }; 6E84E51F28DD9646008CAE85 /* Shortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shortcuts.swift; sourceTree = ""; }; 6E84E52128DD9713008CAE85 /* TestIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestIntent.swift; sourceTree = ""; }; + 6E84E52528DDB0F2008CAE85 /* KeyEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyEntity.swift; sourceTree = ""; }; 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupLockView.swift; sourceTree = ""; }; 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncFetchRequest.swift; sourceTree = ""; }; 6E8BBFE828DD301B00F03735 /* LockIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.extensionkit-extension"; includeInIndex = 0; path = LockIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -461,6 +444,7 @@ 6E84E51F28DD9646008CAE85 /* Shortcuts.swift */, 6E8BBFFF28DD492300F03735 /* UnlockIntent.swift */, 6E8BBFFB28DD438500F03735 /* LockEntity.swift */, + 6E84E52528DDB0F2008CAE85 /* KeyEntity.swift */, 6E8BBFFD28DD491100F03735 /* LockQuery.swift */, 6E84E52128DD9713008CAE85 /* TestIntent.swift */, 6E8BBFEE28DD301B00F03735 /* Info.plist */, @@ -591,13 +575,11 @@ 6EA7767E28D7061600018FA3 /* Frameworks */, 6EA7767F28D7061600018FA3 /* Resources */, 6EA776A828D707FE00018FA3 /* Embed Frameworks */, - 6E8BBFF528DD301B00F03735 /* Embed ExtensionKit Extensions */, ); buildRules = ( ); dependencies = ( 6EA776A228D707FE00018FA3 /* PBXTargetDependency */, - 6E8BBFF028DD301B00F03735 /* PBXTargetDependency */, ); name = SmartLock; productName = SmartLock; @@ -730,13 +712,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 6E84E52428DD97D5008CAE85 /* LockEntity.swift in Sources */, - 6E84E52228DD9713008CAE85 /* TestIntent.swift in Sources */, - 6E84E52328DD97D1008CAE85 /* LockQuery.swift in Sources */, - 6E8BC00028DD492300F03735 /* UnlockIntent.swift in Sources */, - 6E8BBFED28DD301B00F03735 /* ScanLocksIntent.swift in Sources */, 6E8BBFEB28DD301B00F03735 /* LockIntentsExtension.swift in Sources */, - 6E84E52028DD9646008CAE85 /* Shortcuts.swift in Sources */, + 6E84E52E28DDC90B008CAE85 /* TestIntent.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -745,13 +722,19 @@ buildActionMask = 2147483647; files = ( 6E2182F128D7B4FA00A622B3 /* SettingsView.swift in Sources */, + 6E84E52D28DDC841008CAE85 /* LockEntity.swift in Sources */, + 6E84E52728DDC841008CAE85 /* ScanLocksIntent.swift in Sources */, + 6E84E52A28DDC841008CAE85 /* Shortcuts.swift in Sources */, + 6E84E52B28DDC841008CAE85 /* KeyEntity.swift in Sources */, 6E4CB62528D792BF00116573 /* InfoPlist.swift in Sources */, 6E21831D28D834D200A622B3 /* ContentView.swift in Sources */, 6EA7768528D7061600018FA3 /* App.swift in Sources */, 6E2182EF28D7B37000A622B3 /* TabBarView.swift in Sources */, 6E21834C28D9140D00A622B3 /* KeysView.swift in Sources */, + 6E84E52828DDC841008CAE85 /* UnlockIntent.swift in Sources */, 6E21831B28D8341000A622B3 /* SidebarView.swift in Sources */, 6E3276D028D70C9400AF171B /* NearbyDevicesView.swift in Sources */, + 6E84E52C28DDC841008CAE85 /* LockQuery.swift in Sources */, 6E21831F28D84A7300A622B3 /* SidebarLabel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -831,11 +814,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 6E8BBFF028DD301B00F03735 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 6E8BBFE728DD301B00F03735 /* LockIntents */; - targetProxy = 6E8BBFEF28DD301B00F03735 /* PBXContainerItemProxy */; - }; 6E8BBFF928DD30B400F03735 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 6EA7769C28D707FE00018FA3 /* LockKit */; From b35eb5cca4802bd71d71d777c0554fe925a59fcd Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 23 Sep 2022 05:18:23 -0700 Subject: [PATCH 139/229] [App] Added `KeyEntity` --- Xcode/LockIntents/KeyEntity.swift | 78 ++++++++++++++++++++++- Xcode/LockIntents/KeyQuery.swift | 42 ++++++++++++ Xcode/LockIntents/LockEntity.swift | 14 +++- Xcode/LockIntents/LockQuery.swift | 26 ++++++-- Xcode/LockIntents/ScanLocksIntent.swift | 10 ++- Xcode/SmartLock.xcodeproj/project.pbxproj | 4 ++ 6 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 Xcode/LockIntents/KeyQuery.swift diff --git a/Xcode/LockIntents/KeyEntity.swift b/Xcode/LockIntents/KeyEntity.swift index 1468fdf9..36dd087c 100644 --- a/Xcode/LockIntents/KeyEntity.swift +++ b/Xcode/LockIntents/KeyEntity.swift @@ -5,4 +5,80 @@ // Created by Alsey Coleman Miller on 9/23/22. // -import Foundation +import AppIntents +import LockKit + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +struct KeyEntity: AppEntity, Identifiable { + + /// The unique identifier of the key. + var id: UUID + + /// Lock associated with this key. + var lock: UUID + + /// The name of the key. + var name: String + + /// Date key was created. + var created: Date + + /// Key's permissions. + var permission: Permission +} + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +extension KeyEntity { + + static var defaultQuery = KeyQuery() + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Key" + } + + var displayRepresentation: DisplayRepresentation { + return DisplayRepresentation( + title: "\(name)", + subtitle: "\(permission.localizedStringResource)" + ) + } +} + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +extension KeyEntity { + + init(key: Key, lock: UUID) { + self.id = key.id + self.lock = lock + self.name = key.name + self.created = key.created + self.permission = .init(rawValue: key.permission.type.rawValue)! + } +} + +// MARK: - Supporting Types + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +extension KeyEntity { + + enum Permission: UInt8, AppEnum { + + case owner = 0x00 + case admin = 0x01 + case anytime = 0x02 + case scheduled = 0x03 + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Permission" + } + + static var caseDisplayRepresentations: [Permission : DisplayRepresentation] { + [ + .owner: "Owner", + .admin: "Admin", + .anytime: "Anytime", + .scheduled: "Scheduled" + ] + } + } +} diff --git a/Xcode/LockIntents/KeyQuery.swift b/Xcode/LockIntents/KeyQuery.swift new file mode 100644 index 00000000..2e27610b --- /dev/null +++ b/Xcode/LockIntents/KeyQuery.swift @@ -0,0 +1,42 @@ +// +// KeyQuery.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/23/22. +// + +import AppIntents +import LockKit + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +struct KeyQuery: EntityQuery { + + @MainActor + func entities(for identifiers: [UUID]) throws -> [KeyEntity] { + return Store.shared.applicationData.locks + .filter { identifiers.contains($0.value.key.id) } + .map { + KeyEntity( + id: $0.value.key.id, + lock: $0.key, + name: $0.value.key.name, + created: $0.value.key.created, + permission: .init(rawValue: $0.value.key.permission.type.rawValue)! + ) + } + } + + @MainActor + func suggestedEntities() throws -> [KeyEntity] { + return Store.shared.applicationData.locks + .map { + KeyEntity( + id: $0.value.key.id, + lock: $0.key, + name: $0.value.key.name, + created: $0.value.key.created, + permission: .init(rawValue: $0.value.key.permission.type.rawValue)! + ) + } + } +} diff --git a/Xcode/LockIntents/LockEntity.swift b/Xcode/LockIntents/LockEntity.swift index 6c57bfd3..013775fe 100644 --- a/Xcode/LockIntents/LockEntity.swift +++ b/Xcode/LockIntents/LockEntity.swift @@ -25,6 +25,12 @@ struct LockEntity: AppEntity, Identifiable { /// Supported lock actions var unlockActions: Set + + /// Stored name + var name: String? + + /// Associated key + var key: KeyEntity? } @available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) @@ -38,7 +44,7 @@ extension LockEntity { var displayRepresentation: DisplayRepresentation { return DisplayRepresentation( - title: "Lock", + title: "\(name ?? "Lock")", subtitle: "\(id.description)" ) } @@ -47,15 +53,19 @@ extension LockEntity { @available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) extension LockEntity { - init(information: LockInformation) { + init(information: LockInformation, name: String?, key: KeyEntity?) { self.id = information.id self.buildVersion = information.buildVersion.rawValue self.version = information.version.rawValue self.status = .init(rawValue: information.status.rawValue)! self.unlockActions = .init(information.unlockActions.map { .init(rawValue: $0.rawValue)! }) + self.name = name + self.key = key } } +// MARK: - Supporting Typess= + @available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) extension LockEntity { diff --git a/Xcode/LockIntents/LockQuery.swift b/Xcode/LockIntents/LockQuery.swift index 9a5d873f..36e2c11e 100644 --- a/Xcode/LockIntents/LockQuery.swift +++ b/Xcode/LockIntents/LockQuery.swift @@ -11,17 +11,33 @@ import LockKit @available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) struct LockQuery: EntityQuery { - func entities(for identifiers: [UUID]) async throws -> [LockEntity] { - let store = await Store.shared - return await store.applicationData.locks - .filter { identifiers.contains($0.key) } + @MainActor + func entities(for identifiers: [UUID]) throws -> [LockEntity] { + return cachedLocks.filter { identifiers.contains($0.id) } + } + + @MainActor + func suggestedEntities() throws -> [LockEntity] { + return cachedLocks + } +} + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +private extension LockQuery { + + @MainActor + var cachedLocks: [LockEntity] { + return Store.shared.applicationData.locks.lazy + .sorted { $0.value.key.created < $1.value.key.created } .map { LockEntity( id: $0.key, buildVersion: $0.value.information.buildVersion.rawValue, version: $0.value.information.version.rawValue, status: .init(rawValue: $0.value.information.status.rawValue)!, - unlockActions: .init($0.value.information.unlockActions.map { .init(rawValue: $0.rawValue)! }) + unlockActions: .init($0.value.information.unlockActions.map { .init(rawValue: $0.rawValue)! }), + name: $0.value.name, + key: .init(key: $0.value.key, lock: $0.key) ) } } diff --git a/Xcode/LockIntents/ScanLocksIntent.swift b/Xcode/LockIntents/ScanLocksIntent.swift index 75da876d..eb4f392d 100644 --- a/Xcode/LockIntents/ScanLocksIntent.swift +++ b/Xcode/LockIntents/ScanLocksIntent.swift @@ -47,8 +47,16 @@ struct ScanLocksIntent: AppIntent { let locks = store.lockInformation .sorted(by: { $0.key.id.description < $1.key.id.description }) .map { $0.value } + let lockCache = store.applicationData.locks + let results = locks.map { lock in + LockEntity( + information: lock, + name: lockCache[lock.id]?.name, + key: lockCache[lock.id].flatMap { KeyEntity(key: $0.key, lock: lock.id) } + ) + } return .result( - value: locks.map { LockEntity(information: $0) }, + value: results, view: view(for: locks) ) } diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index e4ea8d39..8702bca7 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -89,6 +89,7 @@ 6E84E52C28DDC841008CAE85 /* LockQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFD28DD491100F03735 /* LockQuery.swift */; }; 6E84E52D28DDC841008CAE85 /* LockEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFB28DD438500F03735 /* LockEntity.swift */; }; 6E84E52E28DDC90B008CAE85 /* TestIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E52128DD9713008CAE85 /* TestIntent.swift */; }; + 6E84E53028DDCF4F008CAE85 /* KeyQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E52F28DDCF4F008CAE85 /* KeyQuery.swift */; }; 6E8BBFDA28DC2CA000F03735 /* SetupLockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */; }; 6E8BBFDC28DCC8F300F03735 /* AsyncFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */; }; 6E8BBFEB28DD301B00F03735 /* LockIntentsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFEA28DD301B00F03735 /* LockIntentsExtension.swift */; }; @@ -214,6 +215,7 @@ 6E84E51F28DD9646008CAE85 /* Shortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shortcuts.swift; sourceTree = ""; }; 6E84E52128DD9713008CAE85 /* TestIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestIntent.swift; sourceTree = ""; }; 6E84E52528DDB0F2008CAE85 /* KeyEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyEntity.swift; sourceTree = ""; }; + 6E84E52F28DDCF4F008CAE85 /* KeyQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyQuery.swift; sourceTree = ""; }; 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupLockView.swift; sourceTree = ""; }; 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncFetchRequest.swift; sourceTree = ""; }; 6E8BBFE828DD301B00F03735 /* LockIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.extensionkit-extension"; includeInIndex = 0; path = LockIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -446,6 +448,7 @@ 6E8BBFFB28DD438500F03735 /* LockEntity.swift */, 6E84E52528DDB0F2008CAE85 /* KeyEntity.swift */, 6E8BBFFD28DD491100F03735 /* LockQuery.swift */, + 6E84E52F28DDCF4F008CAE85 /* KeyQuery.swift */, 6E84E52128DD9713008CAE85 /* TestIntent.swift */, 6E8BBFEE28DD301B00F03735 /* Info.plist */, ); @@ -729,6 +732,7 @@ 6E4CB62528D792BF00116573 /* InfoPlist.swift in Sources */, 6E21831D28D834D200A622B3 /* ContentView.swift in Sources */, 6EA7768528D7061600018FA3 /* App.swift in Sources */, + 6E84E53028DDCF4F008CAE85 /* KeyQuery.swift in Sources */, 6E2182EF28D7B37000A622B3 /* TabBarView.swift in Sources */, 6E21834C28D9140D00A622B3 /* KeysView.swift in Sources */, 6E84E52828DDC841008CAE85 /* UnlockIntent.swift in Sources */, From 6156bf40ca57748599f661d74769dc0deba14559 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 23 Sep 2022 05:59:44 -0700 Subject: [PATCH 140/229] [App] Added permission images --- Xcode/LockIntents/ScanLocksIntent.swift | 22 ++++++-- Xcode/LockKit/Assets.xcassets/Contents.json | 6 ++ .../Assets.xcassets/Permissions/Contents.json | 6 ++ .../permissionAdmin.imageset/Contents.json | 15 +++++ .../permissionBadgeAdmin.pdf | Bin 0 -> 2923 bytes .../permissionAnytime.imageset/Contents.json | 15 +++++ .../permissionBadgeAnytime.pdf | Bin 0 -> 3085 bytes .../permissionOwner.imageset/Contents.json | 15 +++++ .../permissionBadgeOwner.pdf | Bin 0 -> 3327 bytes .../Contents.json | 15 +++++ .../permissionBadgeScheduled.pdf | Bin 0 -> 2865 bytes Xcode/LockKit/Extensions/Bundle.swift | 29 ++++++++++ Xcode/LockKit/Model/Permission.swift | 53 ++++++++++++++++++ Xcode/LockKit/View/LockRowView.swift | 14 +---- Xcode/LockKit/View/UIKit/ActivityItem.swift | 11 ++-- Xcode/LockKit/View/UIKit/UIImage.swift | 19 +++++++ Xcode/SmartLock.xcodeproj/project.pbxproj | 16 ++++++ 17 files changed, 213 insertions(+), 23 deletions(-) create mode 100644 Xcode/LockKit/Assets.xcassets/Contents.json create mode 100644 Xcode/LockKit/Assets.xcassets/Permissions/Contents.json create mode 100644 Xcode/LockKit/Assets.xcassets/Permissions/permissionAdmin.imageset/Contents.json create mode 100644 Xcode/LockKit/Assets.xcassets/Permissions/permissionAdmin.imageset/permissionBadgeAdmin.pdf create mode 100644 Xcode/LockKit/Assets.xcassets/Permissions/permissionAnytime.imageset/Contents.json create mode 100644 Xcode/LockKit/Assets.xcassets/Permissions/permissionAnytime.imageset/permissionBadgeAnytime.pdf create mode 100644 Xcode/LockKit/Assets.xcassets/Permissions/permissionOwner.imageset/Contents.json create mode 100644 Xcode/LockKit/Assets.xcassets/Permissions/permissionOwner.imageset/permissionBadgeOwner.pdf create mode 100644 Xcode/LockKit/Assets.xcassets/Permissions/permissionScheduled.imageset/Contents.json create mode 100644 Xcode/LockKit/Assets.xcassets/Permissions/permissionScheduled.imageset/permissionBadgeScheduled.pdf create mode 100644 Xcode/LockKit/Extensions/Bundle.swift create mode 100644 Xcode/LockKit/View/UIKit/UIImage.swift diff --git a/Xcode/LockIntents/ScanLocksIntent.swift b/Xcode/LockIntents/ScanLocksIntent.swift index eb4f392d..7dc19850 100644 --- a/Xcode/LockIntents/ScanLocksIntent.swift +++ b/Xcode/LockIntents/ScanLocksIntent.swift @@ -74,6 +74,7 @@ private extension ScanLocksIntent { } else { ForEach(results) { view(for: $0) + .padding(8) } } } @@ -84,16 +85,25 @@ private extension ScanLocksIntent { func view(for lock: LockInformation) -> some View { if let cache = Store.shared.applicationData.locks[lock.id] { return LockRowView( - image: .emoji("🔒"), //.permission(cache.key.permission.type), + image: .image(Image(permissionType: cache.key.permission.type)), title: cache.name, subtitle: cache.key.permission.type.localizedText ) } else { - return LockRowView( - image: .emoji("🔒"), - title: lock.status == .setup ? "Setup" : "Lock", - subtitle: lock.id.description - ) + switch lock.status { + case .unlock: + return LockRowView( + image: .image(Image(permissionType: .anytime)), + title: "Lock", + subtitle: lock.id.description + ) + case .setup: + return LockRowView( + image: .image(Image(permissionType: .owner)), + title: "Setup", + subtitle: lock.id.description + ) + } } } } diff --git a/Xcode/LockKit/Assets.xcassets/Contents.json b/Xcode/LockKit/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Xcode/LockKit/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Xcode/LockKit/Assets.xcassets/Permissions/Contents.json b/Xcode/LockKit/Assets.xcassets/Permissions/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Xcode/LockKit/Assets.xcassets/Permissions/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Xcode/LockKit/Assets.xcassets/Permissions/permissionAdmin.imageset/Contents.json b/Xcode/LockKit/Assets.xcassets/Permissions/permissionAdmin.imageset/Contents.json new file mode 100644 index 00000000..833800e9 --- /dev/null +++ b/Xcode/LockKit/Assets.xcassets/Permissions/permissionAdmin.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "permissionBadgeAdmin.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Xcode/LockKit/Assets.xcassets/Permissions/permissionAdmin.imageset/permissionBadgeAdmin.pdf b/Xcode/LockKit/Assets.xcassets/Permissions/permissionAdmin.imageset/permissionBadgeAdmin.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e8517475ba509de76110486ce96d1c48c37435c1 GIT binary patch literal 2923 zcmai02{=@1AMYlqN%}q&HMWzQ8nT=-W+B^S3_`XMSsIL)LnCIGC0pvMY!$b1$(p4s z)t3}SWV^{F4T(x2gf^m!o2BGCWBIgv&vVanp0oVk-+ABjzW?9*`~NYv6jOCbLkoqe z9T*)L%p1#mR9lC_10cZh^g-$A0h)VYwm1I(KtLcWplQnT<-=U$?n~#x6qvzb!hnGR zipS@|bU#$kFSK~wxK2w(C4J^V@_Q0ij&Gyf)r1O>y<*yjBa-y2jCI4fkrt-4rw!haq z@Sxs%#egp>b7hh=P8mTg7x#V(!U%B_DWK(r!gYtl{KU4-QpZEl9(xRk!Kw=SBl-Gq zc*mU^2RB&s&c30pZ)!~;kuMACrcc}gTtXlIUf7P0Yb&7!aXowYGma^`lQ}yv*yQ7>!P|| zd_uu&=0Zr3Cy^3F;TPTU1fr->^oaXLBzuGXVL;QC?mc(q!fZaEH7^({%;N~S444NH z=K*65n~${f0FlTLMOnd27M;uq0$f1^AVA1TLupbud_-UffQV~hJkj@yqO|3581^t9 za76~Cm;y9F)0pGS;oAGt889GHtuYS*NOJ=i7<|)*_BVYj2wP{4rD=`)u%?kn+-%Xn zniTjDivd&3$p5VQ2nW|_IfqSO<8>NP^QZB2U~uL)B<_UhEtN^_&T73!;wJr1dPD)E_m+14>|P#J4b9AtzYH!1l` zH=eoO*(VilK0G$lJ;iyGu{k^Z7P+F}^Tuq0Qri3Uva>TUAuGTJZ}j-0arG==7NVdYy19 zVvj_3K}U=HM+W6fE?mpL@o_MA2s1Z(>z$*A!%Qs`JK^pQNq7Tkrj0 zFJ}AbhA#u>QBgOmFIHD^^;O2BF*B|uMd|WR^^dfaH9T)FwdB(qbe zn%r*he=L=&J8h%tW%1E-uAn>E~zr4@(Hh7 z{mR3;-QZcEOlaB0n8R$qqNBMnx?Z7cxldIz+E3MEM*Jk&xge^tK$Mk z(YkK^ipPaT?^U-??+WR6(_i;Rrc}}Cewkvse;77AW4YXF9&3hf=rgYx==v1bxUqX85*Y}TdS}}~^Hin7p2l$j&`;ZNvOLVF zOzV?tg(c6DoODz<=amxsMy00gf}c95j7IBW^7?6ozJblcR8CF_cYFYwE%?lPlv(YqsIl|k5k zbGi5ZrPN1)`;ha=26Z+uIN)4up#9L+vSeOmRHffOy{03P2WH)aCc;Jn{zOxD%Cyw? zDUu=s2gF9C(?iLANy6KPqf?bzZ(4*-XirpTVPCo%mbj4qQj`libzn^BIG8cvdQG^l z=jidb$G$3MW*m0-*1BQki4`u=)52FIKMT*F07j1FL zhIv04D-ps0BpuwGQwk7!h5$`V7L$iKsL02W{=RWZNaV!d_i0S$(|tMK^B&JzF!v>I zhM6SR-E^91I{%5Bt&CfECNv7v?U}7K;|FEuF>Y8;dFvoCaP4T4F3F zF)U>l4upsx9)|}(Eg0PIB!B#@8*e2+mo+CTF^91?-=c_4xCGe*=#Cip!M(m`_b4;V=L8H04R ze&XRkBBJi^`@=!d|G@}|x_{sy#Gf$&BGDgt+Q^zOVtg*0g{Wh$g0*LbAZtwokfMTv z2wx;_BvR&VFAlN`=AS9ZKF}j-!FZy!4v6z&;P74`lMdlPZIBMa5Dunm)9D7Nf2Pdu WBOV_S^IR}-csvn>!I;>XqW%k(MRN@R literal 0 HcmV?d00001 diff --git a/Xcode/LockKit/Assets.xcassets/Permissions/permissionAnytime.imageset/Contents.json b/Xcode/LockKit/Assets.xcassets/Permissions/permissionAnytime.imageset/Contents.json new file mode 100644 index 00000000..b200c93c --- /dev/null +++ b/Xcode/LockKit/Assets.xcassets/Permissions/permissionAnytime.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "permissionBadgeAnytime.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Xcode/LockKit/Assets.xcassets/Permissions/permissionAnytime.imageset/permissionBadgeAnytime.pdf b/Xcode/LockKit/Assets.xcassets/Permissions/permissionAnytime.imageset/permissionBadgeAnytime.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ec6e6a153fc92c6fff882edcb916bbd7abe69408 GIT binary patch literal 3085 zcmai0c|25Y8}6l2( zd?aLAEJG>9j1Dr==Vla-#Oyb^2d9A&+nY)T+jXN*L~eL&e_^lA0irJaCdu0 zdi(Q7&pf(&4?_k(fbX*hV`>T*Y=L=xB7cB_Kn%dZmKz{~1;{smC4#MCHlG6n=H?io zNC2|}F`>T&A2mr;gBB-D(Bt1KCyyyzGS+S*D1b_Nb@ST>CSyN0A0p2`B$=-llUsLs zS>L(xjKhzIp@%~qTD=kDE|t*}c_U+b1;vRN9k*7!I3cC{irCv+E50DPWaVDU$y}`= z8(Y_YN1BOhABR`n==fLIN!`QQ%Ux67L`&K?wJ$%?roZ1~uNki4>tQ*UC5eLvdz{j! zjX8as!7U=w+FS3+@AtO7~pvv!ZdNqGp7svGS3Ho{e9goUdiF^4*n}is$&&?_J%=<}-R!g*-Z{$c*S_$k+gI-WlKnAyc5AO&Xx9|_XE$V9 zwYEI;y(lMEE>ee?8|Y(8#qPhYW5j!3Tc!yK%jyrE`{-WsB`fu%y{9?_h zqf%$Zu2m;IyrQwsdTfewx->V1U)%2M4`B|d=WEYB4Ae-x-ZNzOLBwqH$mGiZV7=4M z{@v5JRv774UdwZ>e^T%nSJhu5Q+3!EeRM6e#Pn>Jh7ER5(-}o`8Wc7LqGqr5MMa{b-=YEXRm+|xE zlk%!=`1=@`$C;)aQbU=F(i8nsAu8I4R!QHHczak73>Y}G{HERlm?r`ZzY&@N3;DqU zHY^0F-vBEOuqd~v=s%QL-n zFShtui;6`_dOD|JV7+Y1<3&+~xK{_SRN7mVSLiQ@dmLWfsxZO0 zMK`k}Qkhb;rEcM5mFLDZCvb=MV8GB0{!KMK!DigZ3x!Iz(q>%e3hUUCJugZR%ulFx z(h~Q}9aK+nT)*IRuB4ZmkdmPKe2ik3-dKBvzDVy%hUN0md7+Wl5)D3f{=U>%;&eo< zy?MLMhqZfK9kOH^RuMN7Z`y^?ZsgINOGcl^2G?DdY1|85e7%aHcX!^Yx!&6r9iVsG z{c)9YJ${QqN8$4(^tm*juQY&p52 zyw304cHD*$<JbCN27na+nK+@z^UB5NhfEO$pliF@=PIZrc5SO`0?v5W&pAy#O6q{Yz zsjHZe)%S6IaK@2=x3f=vwNrb=F(q!^{;nbcs2;+U{#nwPO9)NLN|n}R1s{Ev!z{3b zi{>*!N(r@=THEAL5k4riCHK3WT{Vd9eE79HKc_w=^)yzE#VS+%I?<;0yz#=b#X18z zf0IoluXg*jnLiCtiMX}$KqL=vc-~l_RHyMmZckMbHjuD;a$YjlqcE|uQ13AHo@xE+ z(Rq#)BlkMYDjpY=yd`Y-WE|e(ZMN*Q$_-8T+FP1!K~ebF({gHKo_755oqJAIJc0Ky zuv<%8^Ph*SFkRux7T6>e!V` z&(+3;gV(yW?!+YQxHA;Laq(ATg?gC#@Q-U9>mFG;zAdcPRc9p06qnz_ED8u|lxFkKiv=Hg@wveh z!q@JvJes_mjO05ePt1`XY@^AK*14bS9_HqD_`F5rZT(q)}7``^TU(K>TF()Cw~MTH2}HwdKBXEYC$Fcd>{&fdz(k|l&W0IFdOfH`A= z>gce0=NSZNQ59F z8Q2!g5{SY9M9-%}M5t)C)65?676C#y3_!FV4rK8FM{Ym>ESNQ$NvrVAEG|!Eh2~Bq zBAE!yTFitoytOe2gs32yLh>UFTM~noa zXYeBi(tpKB2>7!di9|y9{bOGeiS{dINJ7Z>lbs>$*R_y;T{DUN{eDFP78ha1RF3V! z4M)zJ3Lt3(AE7=Mn1pk literal 0 HcmV?d00001 diff --git a/Xcode/LockKit/Assets.xcassets/Permissions/permissionOwner.imageset/Contents.json b/Xcode/LockKit/Assets.xcassets/Permissions/permissionOwner.imageset/Contents.json new file mode 100644 index 00000000..60ef4871 --- /dev/null +++ b/Xcode/LockKit/Assets.xcassets/Permissions/permissionOwner.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "permissionBadgeOwner.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Xcode/LockKit/Assets.xcassets/Permissions/permissionOwner.imageset/permissionBadgeOwner.pdf b/Xcode/LockKit/Assets.xcassets/Permissions/permissionOwner.imageset/permissionBadgeOwner.pdf new file mode 100644 index 0000000000000000000000000000000000000000..838f64ba89bd4f362c908ce018e5077b87905a40 GIT binary patch literal 3327 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 MNGLSg%E=n_FZ0<`UH||9 literal 0 HcmV?d00001 diff --git a/Xcode/LockKit/Extensions/Bundle.swift b/Xcode/LockKit/Extensions/Bundle.swift new file mode 100644 index 00000000..dfdac541 --- /dev/null +++ b/Xcode/LockKit/Extensions/Bundle.swift @@ -0,0 +1,29 @@ +// +// Bundle.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/23/22. +// + +import Foundation + +public extension Bundle { + + /// LockKit Bundle + static var lockKit: Bundle { + struct Cache { + static let bundle = Bundle(for: Store.self) + } + return Cache.bundle + } +} + +public extension Bundle { + + enum Lock: String { + + case app = "com.colemancda.Lock" + case watch = "com.colemancda.Lock.watchkitapp.watchkitextension" + case lockKit = "com.colemancda.LockKit" + } +} diff --git a/Xcode/LockKit/Model/Permission.swift b/Xcode/LockKit/Model/Permission.swift index 44e44972..e5193118 100644 --- a/Xcode/LockKit/Model/Permission.swift +++ b/Xcode/LockKit/Model/Permission.swift @@ -90,3 +90,56 @@ public extension Permission.Schedule.Weekdays.Day { } } } + +// MARK: - Image + +public extension PermissionType { + + enum Image: String { + + case owner = "permissionOwner" + case admin = "permissionAdmin" + case anytime = "permissionAnytime" + case scheduled = "permissionScheduled" + } +} + +public extension PermissionType.Image { + + init(permissionType: PermissionType) { + switch permissionType { + case .owner: + self = .owner + case .admin: + self = .admin + case .anytime: + self = .anytime + case .scheduled: + self = .scheduled + } + } +} + +#if canImport(SwiftUI) +import SwiftUI + +public extension Image { + + init(permissionType: PermissionType) { + let image = PermissionType.Image(permissionType: permissionType) + self.init(image.rawValue, bundle: .lockKit) + } +} +#endif + +#if canImport(UIKit) +import UIKit + +public extension UIImage { + + convenience init(permissionType: PermissionType) { + let image = PermissionType.Image(permissionType: permissionType) + self.init(named: image.rawValue, in: .lockKit, with: nil)! + } +} +#endif diff --git a/Xcode/LockKit/View/LockRowView.swift b/Xcode/LockKit/View/LockRowView.swift index f81c9ba6..1b16e59f 100644 --- a/Xcode/LockKit/View/LockRowView.swift +++ b/Xcode/LockKit/View/LockRowView.swift @@ -68,11 +68,7 @@ public extension LockRowView { case permission(PermissionType) case emoji(Character) case symbol(String) - #if canImport(UIKit) - case image(UIImage) - #elseif canImport(AppKit) - case image(NSImage) - #endif + case image(SwiftUI.Image) } } @@ -108,15 +104,9 @@ extension LockRowView { .font(.system(size: 40)) ) case let .image(image): - #if canImport(UIKit) AnyView( - SwiftUI.Image(uiImage: image) + image ) - #elseif canImport(AppKit) - AnyView( - SwiftUI.Image(nsImage: image) - ) - #endif } } } diff --git a/Xcode/LockKit/View/UIKit/ActivityItem.swift b/Xcode/LockKit/View/UIKit/ActivityItem.swift index 4e7fcfc1..9b438c4f 100644 --- a/Xcode/LockKit/View/UIKit/ActivityItem.swift +++ b/Xcode/LockKit/View/UIKit/ActivityItem.swift @@ -8,6 +8,7 @@ #if os(iOS) import Foundation import UIKit +import LinkPresentation public final class NewKeyFileActivityItem: UIActivityItemProvider { @@ -62,19 +63,19 @@ public final class NewKeyFileActivityItem: UIActivityItemProvider { suggestedSize size: CGSize ) -> UIImage? { - return UIImage.permissionType(invitation.key.permission.type, size: size) + return UIImage(permissionType: invitation.key.permission.type) } - /* - @available(iOSApplicationExtension 13.0, *) + public override func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? { - let permissionImageURL = AssetExtractor.shared.url(for: invitation.key.permission.type.image) + let imageName = PermissionType.Image(permissionType: invitation.key.permission.type) + let permissionImageURL = AssetExtractor.shared.url(for: imageName.rawValue, in: .lockKit) assert(permissionImageURL != nil, "Missing permission image") let metadata = LPLinkMetadata() metadata.title = invitation.key.name metadata.imageProvider = permissionImageURL.flatMap { NSItemProvider(contentsOf: $0) } return metadata - }*/ + } } public extension NewKeyFileActivityItem { diff --git a/Xcode/LockKit/View/UIKit/UIImage.swift b/Xcode/LockKit/View/UIKit/UIImage.swift new file mode 100644 index 00000000..3dc15bf1 --- /dev/null +++ b/Xcode/LockKit/View/UIKit/UIImage.swift @@ -0,0 +1,19 @@ +// +// UIImage.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/23/22. +// + +#if canImport(UIKit) +import Foundation +import UIKit + +public extension UIImage { + + @available(iOS 8.0, watchOS 6.0, *) + convenience init?(lockKit name: String) { + self.init(named: name, in: .lockKit, compatibleWith: nil) + } +} +#endif diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 8702bca7..7f25a2ed 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -90,6 +90,10 @@ 6E84E52D28DDC841008CAE85 /* LockEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFB28DD438500F03735 /* LockEntity.swift */; }; 6E84E52E28DDC90B008CAE85 /* TestIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E52128DD9713008CAE85 /* TestIntent.swift */; }; 6E84E53028DDCF4F008CAE85 /* KeyQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E52F28DDCF4F008CAE85 /* KeyQuery.swift */; }; + 6E84E53228DDDC11008CAE85 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6E84E53128DDDC11008CAE85 /* Assets.xcassets */; }; + 6E84E53428DDDCDC008CAE85 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E53328DDDCDC008CAE85 /* Bundle.swift */; }; + 6E84E53628DDE01F008CAE85 /* AssetExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E53528DDE01F008CAE85 /* AssetExtractor.swift */; }; + 6E84E53828DDE08A008CAE85 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E53728DDE08A008CAE85 /* UIImage.swift */; }; 6E8BBFDA28DC2CA000F03735 /* SetupLockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */; }; 6E8BBFDC28DCC8F300F03735 /* AsyncFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */; }; 6E8BBFEB28DD301B00F03735 /* LockIntentsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFEA28DD301B00F03735 /* LockIntentsExtension.swift */; }; @@ -216,6 +220,10 @@ 6E84E52128DD9713008CAE85 /* TestIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestIntent.swift; sourceTree = ""; }; 6E84E52528DDB0F2008CAE85 /* KeyEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyEntity.swift; sourceTree = ""; }; 6E84E52F28DDCF4F008CAE85 /* KeyQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyQuery.swift; sourceTree = ""; }; + 6E84E53128DDDC11008CAE85 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 6E84E53328DDDCDC008CAE85 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; + 6E84E53528DDE01F008CAE85 /* AssetExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExtractor.swift; sourceTree = ""; }; + 6E84E53728DDE08A008CAE85 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupLockView.swift; sourceTree = ""; }; 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncFetchRequest.swift; sourceTree = ""; }; 6E8BBFE828DD301B00F03735 /* LockIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.extensionkit-extension"; includeInIndex = 0; path = LockIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -308,6 +316,7 @@ children = ( 6E21834528D8FC4300A622B3 /* Task.swift */, 6E21834928D90F7A00A622B3 /* LocalAuthentication.swift */, + 6E84E53328DDDCDC008CAE85 /* Bundle.swift */, ); path = Extensions; sourceTree = ""; @@ -384,6 +393,7 @@ 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */, 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */, 6E6A97F028DBEE0700C689F6 /* Predicate.swift */, + 6E84E53528DDE01F008CAE85 /* AssetExtractor.swift */, 6E21830F28D80DCD00A622B3 /* Keychain.swift */, 6E21831628D8116C00A622B3 /* NewKey.swift */, 6E21831728D8116C00A622B3 /* NewKeyDocument.swift */, @@ -418,6 +428,7 @@ isa = PBXGroup; children = ( 6E4CB61828D788AA00116573 /* UIStyleKit.swift */, + 6E84E53728DDE08A008CAE85 /* UIImage.swift */, 6E4CB62028D7907200116573 /* PermissionIconViewUIView.swift */, 6E21830228D7C47500A622B3 /* Appearance.swift */, 6ED248CB28D7A3BF00F78D07 /* ActivityIndicatorView.swift */, @@ -506,6 +517,7 @@ children = ( 6EA7769F28D707FE00018FA3 /* LockKit.h */, 6E21830428D7C51900A622B3 /* Log.swift */, + 6E84E53128DDDC11008CAE85 /* Assets.xcassets */, 6E21834428D8FC3700A622B3 /* Extensions */, 6E4CB60F28D7866600116573 /* View */, 6E3276DA28D7136400AF171B /* Model */, @@ -697,6 +709,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6E84E53228DDDC11008CAE85 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -766,6 +779,7 @@ 6E21831028D80DCD00A622B3 /* Keychain.swift in Sources */, 6E21833C28D8F3B500A622B3 /* KeyManagedObject.swift in Sources */, 6E21830128D7C37500A622B3 /* Error.swift in Sources */, + 6E84E53428DDDCDC008CAE85 /* Bundle.swift in Sources */, 6E21836328D9516B00A622B3 /* iCloud.swift in Sources */, 6E21833E28D8F3B500A622B3 /* CreateNewKeyEventManagedObject.swift in Sources */, 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */, @@ -786,6 +800,7 @@ 6E8BBFDA28DC2CA000F03735 /* SetupLockView.swift in Sources */, 6E9E37AD28DADF2600BE7128 /* ActivityView.swift in Sources */, 6E21834328D8F3B500A622B3 /* ManagedObject.swift in Sources */, + 6E84E53628DDE01F008CAE85 /* AssetExtractor.swift in Sources */, 6E21834028D8F3B500A622B3 /* RemoveKeyEventManagedObject.swift in Sources */, 6E21834A28D90F7A00A622B3 /* LocalAuthentication.swift in Sources */, 6E21833628D8F3B500A622B3 /* ContactManagedObject.swift in Sources */, @@ -802,6 +817,7 @@ 6E6A97F328DC030000C689F6 /* NewKeyInvitationView.swift in Sources */, 6E21831528D80FF900A622B3 /* ApplicationData.swift in Sources */, 6E21833A28D8F3B500A622B3 /* LockManagedObject.swift in Sources */, + 6E84E53828DDE08A008CAE85 /* UIImage.swift in Sources */, 6E21830328D7C47500A622B3 /* Appearance.swift in Sources */, 6E3276DC28D7195400AF171B /* Central.swift in Sources */, 6E21833928D8F3B500A622B3 /* ScheduleManagedObject.swift in Sources */, From 8f023dd3d6d6f0bdf304dcdea68b01bd44b13b39 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 23 Sep 2022 05:59:57 -0700 Subject: [PATCH 141/229] [App] Added `AssetExtractor` --- Xcode/LockKit/Model/AssetExtractor.swift | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 Xcode/LockKit/Model/AssetExtractor.swift diff --git a/Xcode/LockKit/Model/AssetExtractor.swift b/Xcode/LockKit/Model/AssetExtractor.swift new file mode 100644 index 00000000..c1962510 --- /dev/null +++ b/Xcode/LockKit/Model/AssetExtractor.swift @@ -0,0 +1,45 @@ +// +// AssetExtractor.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/23/22. +// + +#if canImport(UIKit) +import Foundation +import UIKit + +/// Get URL from asset. +@available(iOS 8.0, watchOS 6.0, *) +public final class AssetExtractor { + + public static let shared = AssetExtractor() + + private init() { } + + private lazy var fileManager = FileManager() + + private lazy var cachesDirectory: URL = { + guard let url = fileManager.cachesDirectory + else { fatalError("Could not load cache directory") } + return url + }() + + @available(iOS 8.0, watchOS 6.0, *) + public func url(for imageName: String, in bundle: Bundle = .lockKit) -> URL? { + + let fileName = (bundle.bundleIdentifier ?? bundle.bundleURL.lastPathComponent) + "." + imageName + ".png" + let url = cachesDirectory.appendingPathComponent(fileName) + + if fileManager.fileExists(atPath: url.path) == false { + guard let image = UIImage(named: imageName, in: bundle, compatibleWith: nil) + else { return nil } + guard let imageData = image.pngData() + else { fatalError("Could not convert image to PNG") } + fileManager.createFile(atPath: url.path, contents: imageData, attributes: nil) + } + + return url + } +} +#endif From e1722b0c37770cb2dc8a843accddf55c7e202c5f Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 23 Sep 2022 17:09:17 -0700 Subject: [PATCH 142/229] [App] Fixed shortcut image --- Xcode/LockIntents/KeyEntity.swift | 16 +++++++++++++++- Xcode/LockIntents/LockEntity.swift | 12 +++++++++++- .../Assets.xcassets/Permissions/Contents.json | 6 ++++++ .../permissionAdmin.imageset/Contents.json | 15 +++++++++++++++ .../permissionBadgeAdmin.pdf | Bin 0 -> 2923 bytes .../permissionAnytime.imageset/Contents.json | 15 +++++++++++++++ .../permissionBadgeAnytime.pdf | Bin 0 -> 3085 bytes .../permissionOwner.imageset/Contents.json | 15 +++++++++++++++ .../permissionBadgeOwner.pdf | Bin 0 -> 3327 bytes .../permissionScheduled.imageset/Contents.json | 15 +++++++++++++++ .../permissionBadgeScheduled.pdf | Bin 0 -> 2865 bytes 11 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 Xcode/SmartLock/Assets.xcassets/Permissions/Contents.json create mode 100644 Xcode/SmartLock/Assets.xcassets/Permissions/permissionAdmin.imageset/Contents.json create mode 100644 Xcode/SmartLock/Assets.xcassets/Permissions/permissionAdmin.imageset/permissionBadgeAdmin.pdf create mode 100644 Xcode/SmartLock/Assets.xcassets/Permissions/permissionAnytime.imageset/Contents.json create mode 100644 Xcode/SmartLock/Assets.xcassets/Permissions/permissionAnytime.imageset/permissionBadgeAnytime.pdf create mode 100644 Xcode/SmartLock/Assets.xcassets/Permissions/permissionOwner.imageset/Contents.json create mode 100644 Xcode/SmartLock/Assets.xcassets/Permissions/permissionOwner.imageset/permissionBadgeOwner.pdf create mode 100644 Xcode/SmartLock/Assets.xcassets/Permissions/permissionScheduled.imageset/Contents.json create mode 100644 Xcode/SmartLock/Assets.xcassets/Permissions/permissionScheduled.imageset/permissionBadgeScheduled.pdf diff --git a/Xcode/LockIntents/KeyEntity.swift b/Xcode/LockIntents/KeyEntity.swift index 36dd087c..cf22ffd7 100644 --- a/Xcode/LockIntents/KeyEntity.swift +++ b/Xcode/LockIntents/KeyEntity.swift @@ -39,7 +39,8 @@ extension KeyEntity { var displayRepresentation: DisplayRepresentation { return DisplayRepresentation( title: "\(name)", - subtitle: "\(permission.localizedStringResource)" + subtitle: "\(permission.localizedStringResource)", + image: .init(named: permission.imageName, isTemplate: false) ) } } @@ -80,5 +81,18 @@ extension KeyEntity { .scheduled: "Scheduled" ] } + + var imageName: String { + switch self { + case .owner: + return "permissionOwner" + case .admin: + return "permissionAdmin" + case .anytime: + return "permissionAnytime" + case .scheduled: + return "permissionScheduled" + } + } } } diff --git a/Xcode/LockIntents/LockEntity.swift b/Xcode/LockIntents/LockEntity.swift index 013775fe..a31d147c 100644 --- a/Xcode/LockIntents/LockEntity.swift +++ b/Xcode/LockIntents/LockEntity.swift @@ -43,13 +43,23 @@ extension LockEntity { } var displayRepresentation: DisplayRepresentation { + let permission = self.key?.permission ?? .anytime return DisplayRepresentation( title: "\(name ?? "Lock")", - subtitle: "\(id.description)" + subtitle: "\(id.description)", + image: .init(named: permission.imageName, isTemplate: false) ) } } +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +private extension LockEntity { + + var image: PermissionType.Image { + PermissionType.Image(permissionType: .init(rawValue: (key?.permission ?? .anytime).rawValue)!) + } +} + @available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) extension LockEntity { diff --git a/Xcode/SmartLock/Assets.xcassets/Permissions/Contents.json b/Xcode/SmartLock/Assets.xcassets/Permissions/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Xcode/SmartLock/Assets.xcassets/Permissions/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Xcode/SmartLock/Assets.xcassets/Permissions/permissionAdmin.imageset/Contents.json b/Xcode/SmartLock/Assets.xcassets/Permissions/permissionAdmin.imageset/Contents.json new file mode 100644 index 00000000..833800e9 --- /dev/null +++ b/Xcode/SmartLock/Assets.xcassets/Permissions/permissionAdmin.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "permissionBadgeAdmin.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Xcode/SmartLock/Assets.xcassets/Permissions/permissionAdmin.imageset/permissionBadgeAdmin.pdf b/Xcode/SmartLock/Assets.xcassets/Permissions/permissionAdmin.imageset/permissionBadgeAdmin.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e8517475ba509de76110486ce96d1c48c37435c1 GIT binary patch literal 2923 zcmai02{=@1AMYlqN%}q&HMWzQ8nT=-W+B^S3_`XMSsIL)LnCIGC0pvMY!$b1$(p4s z)t3}SWV^{F4T(x2gf^m!o2BGCWBIgv&vVanp0oVk-+ABjzW?9*`~NYv6jOCbLkoqe z9T*)L%p1#mR9lC_10cZh^g-$A0h)VYwm1I(KtLcWplQnT<-=U$?n~#x6qvzb!hnGR zipS@|bU#$kFSK~wxK2w(C4J^V@_Q0ij&Gyf)r1O>y<*yjBa-y2jCI4fkrt-4rw!haq z@Sxs%#egp>b7hh=P8mTg7x#V(!U%B_DWK(r!gYtl{KU4-QpZEl9(xRk!Kw=SBl-Gq zc*mU^2RB&s&c30pZ)!~;kuMACrcc}gTtXlIUf7P0Yb&7!aXowYGma^`lQ}yv*yQ7>!P|| zd_uu&=0Zr3Cy^3F;TPTU1fr->^oaXLBzuGXVL;QC?mc(q!fZaEH7^({%;N~S444NH z=K*65n~${f0FlTLMOnd27M;uq0$f1^AVA1TLupbud_-UffQV~hJkj@yqO|3581^t9 za76~Cm;y9F)0pGS;oAGt889GHtuYS*NOJ=i7<|)*_BVYj2wP{4rD=`)u%?kn+-%Xn zniTjDivd&3$p5VQ2nW|_IfqSO<8>NP^QZB2U~uL)B<_UhEtN^_&T73!;wJr1dPD)E_m+14>|P#J4b9AtzYH!1l` zH=eoO*(VilK0G$lJ;iyGu{k^Z7P+F}^Tuq0Qri3Uva>TUAuGTJZ}j-0arG==7NVdYy19 zVvj_3K}U=HM+W6fE?mpL@o_MA2s1Z(>z$*A!%Qs`JK^pQNq7Tkrj0 zFJ}AbhA#u>QBgOmFIHD^^;O2BF*B|uMd|WR^^dfaH9T)FwdB(qbe zn%r*he=L=&J8h%tW%1E-uAn>E~zr4@(Hh7 z{mR3;-QZcEOlaB0n8R$qqNBMnx?Z7cxldIz+E3MEM*Jk&xge^tK$Mk z(YkK^ipPaT?^U-??+WR6(_i;Rrc}}Cewkvse;77AW4YXF9&3hf=rgYx==v1bxUqX85*Y}TdS}}~^Hin7p2l$j&`;ZNvOLVF zOzV?tg(c6DoODz<=amxsMy00gf}c95j7IBW^7?6ozJblcR8CF_cYFYwE%?lPlv(YqsIl|k5k zbGi5ZrPN1)`;ha=26Z+uIN)4up#9L+vSeOmRHffOy{03P2WH)aCc;Jn{zOxD%Cyw? zDUu=s2gF9C(?iLANy6KPqf?bzZ(4*-XirpTVPCo%mbj4qQj`libzn^BIG8cvdQG^l z=jidb$G$3MW*m0-*1BQki4`u=)52FIKMT*F07j1FL zhIv04D-ps0BpuwGQwk7!h5$`V7L$iKsL02W{=RWZNaV!d_i0S$(|tMK^B&JzF!v>I zhM6SR-E^91I{%5Bt&CfECNv7v?U}7K;|FEuF>Y8;dFvoCaP4T4F3F zF)U>l4upsx9)|}(Eg0PIB!B#@8*e2+mo+CTF^91?-=c_4xCGe*=#Cip!M(m`_b4;V=L8H04R ze&XRkBBJi^`@=!d|G@}|x_{sy#Gf$&BGDgt+Q^zOVtg*0g{Wh$g0*LbAZtwokfMTv z2wx;_BvR&VFAlN`=AS9ZKF}j-!FZy!4v6z&;P74`lMdlPZIBMa5Dunm)9D7Nf2Pdu WBOV_S^IR}-csvn>!I;>XqW%k(MRN@R literal 0 HcmV?d00001 diff --git a/Xcode/SmartLock/Assets.xcassets/Permissions/permissionAnytime.imageset/Contents.json b/Xcode/SmartLock/Assets.xcassets/Permissions/permissionAnytime.imageset/Contents.json new file mode 100644 index 00000000..b200c93c --- /dev/null +++ b/Xcode/SmartLock/Assets.xcassets/Permissions/permissionAnytime.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "permissionBadgeAnytime.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Xcode/SmartLock/Assets.xcassets/Permissions/permissionAnytime.imageset/permissionBadgeAnytime.pdf b/Xcode/SmartLock/Assets.xcassets/Permissions/permissionAnytime.imageset/permissionBadgeAnytime.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ec6e6a153fc92c6fff882edcb916bbd7abe69408 GIT binary patch literal 3085 zcmai0c|25Y8}6l2( zd?aLAEJG>9j1Dr==Vla-#Oyb^2d9A&+nY)T+jXN*L~eL&e_^lA0irJaCdu0 zdi(Q7&pf(&4?_k(fbX*hV`>T*Y=L=xB7cB_Kn%dZmKz{~1;{smC4#MCHlG6n=H?io zNC2|}F`>T&A2mr;gBB-D(Bt1KCyyyzGS+S*D1b_Nb@ST>CSyN0A0p2`B$=-llUsLs zS>L(xjKhzIp@%~qTD=kDE|t*}c_U+b1;vRN9k*7!I3cC{irCv+E50DPWaVDU$y}`= z8(Y_YN1BOhABR`n==fLIN!`QQ%Ux67L`&K?wJ$%?roZ1~uNki4>tQ*UC5eLvdz{j! zjX8as!7U=w+FS3+@AtO7~pvv!ZdNqGp7svGS3Ho{e9goUdiF^4*n}is$&&?_J%=<}-R!g*-Z{$c*S_$k+gI-WlKnAyc5AO&Xx9|_XE$V9 zwYEI;y(lMEE>ee?8|Y(8#qPhYW5j!3Tc!yK%jyrE`{-WsB`fu%y{9?_h zqf%$Zu2m;IyrQwsdTfewx->V1U)%2M4`B|d=WEYB4Ae-x-ZNzOLBwqH$mGiZV7=4M z{@v5JRv774UdwZ>e^T%nSJhu5Q+3!EeRM6e#Pn>Jh7ER5(-}o`8Wc7LqGqr5MMa{b-=YEXRm+|xE zlk%!=`1=@`$C;)aQbU=F(i8nsAu8I4R!QHHczak73>Y}G{HERlm?r`ZzY&@N3;DqU zHY^0F-vBEOuqd~v=s%QL-n zFShtui;6`_dOD|JV7+Y1<3&+~xK{_SRN7mVSLiQ@dmLWfsxZO0 zMK`k}Qkhb;rEcM5mFLDZCvb=MV8GB0{!KMK!DigZ3x!Iz(q>%e3hUUCJugZR%ulFx z(h~Q}9aK+nT)*IRuB4ZmkdmPKe2ik3-dKBvzDVy%hUN0md7+Wl5)D3f{=U>%;&eo< zy?MLMhqZfK9kOH^RuMN7Z`y^?ZsgINOGcl^2G?DdY1|85e7%aHcX!^Yx!&6r9iVsG z{c)9YJ${QqN8$4(^tm*juQY&p52 zyw304cHD*$<JbCN27na+nK+@z^UB5NhfEO$pliF@=PIZrc5SO`0?v5W&pAy#O6q{Yz zsjHZe)%S6IaK@2=x3f=vwNrb=F(q!^{;nbcs2;+U{#nwPO9)NLN|n}R1s{Ev!z{3b zi{>*!N(r@=THEAL5k4riCHK3WT{Vd9eE79HKc_w=^)yzE#VS+%I?<;0yz#=b#X18z zf0IoluXg*jnLiCtiMX}$KqL=vc-~l_RHyMmZckMbHjuD;a$YjlqcE|uQ13AHo@xE+ z(Rq#)BlkMYDjpY=yd`Y-WE|e(ZMN*Q$_-8T+FP1!K~ebF({gHKo_755oqJAIJc0Ky zuv<%8^Ph*SFkRux7T6>e!V` z&(+3;gV(yW?!+YQxHA;Laq(ATg?gC#@Q-U9>mFG;zAdcPRc9p06qnz_ED8u|lxFkKiv=Hg@wveh z!q@JvJes_mjO05ePt1`XY@^AK*14bS9_HqD_`F5rZT(q)}7``^TU(K>TF()Cw~MTH2}HwdKBXEYC$Fcd>{&fdz(k|l&W0IFdOfH`A= z>gce0=NSZNQ59F z8Q2!g5{SY9M9-%}M5t)C)65?676C#y3_!FV4rK8FM{Ym>ESNQ$NvrVAEG|!Eh2~Bq zBAE!yTFitoytOe2gs32yLh>UFTM~noa zXYeBi(tpKB2>7!di9|y9{bOGeiS{dINJ7Z>lbs>$*R_y;T{DUN{eDFP78ha1RF3V! z4M)zJ3Lt3(AE7=Mn1pk literal 0 HcmV?d00001 diff --git a/Xcode/SmartLock/Assets.xcassets/Permissions/permissionOwner.imageset/Contents.json b/Xcode/SmartLock/Assets.xcassets/Permissions/permissionOwner.imageset/Contents.json new file mode 100644 index 00000000..60ef4871 --- /dev/null +++ b/Xcode/SmartLock/Assets.xcassets/Permissions/permissionOwner.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "permissionBadgeOwner.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Xcode/SmartLock/Assets.xcassets/Permissions/permissionOwner.imageset/permissionBadgeOwner.pdf b/Xcode/SmartLock/Assets.xcassets/Permissions/permissionOwner.imageset/permissionBadgeOwner.pdf new file mode 100644 index 0000000000000000000000000000000000000000..838f64ba89bd4f362c908ce018e5077b87905a40 GIT binary patch literal 3327 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 MNGLSg%E=n_FZ0<`UH||9 literal 0 HcmV?d00001 From 1f73021ff5258455fcebc63ba0449a44a9264f54 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 23 Sep 2022 17:09:37 -0700 Subject: [PATCH 143/229] [App] Fixed CloudKit query operation --- Xcode/LockKit/Model/iCloud/CloudKit.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Xcode/LockKit/Model/iCloud/CloudKit.swift b/Xcode/LockKit/Model/iCloud/CloudKit.swift index a923b24a..e2b71a61 100644 --- a/Xcode/LockKit/Model/iCloud/CloudKit.swift +++ b/Xcode/LockKit/Model/iCloud/CloudKit.swift @@ -287,7 +287,7 @@ internal extension CKDatabase { while let queryCursor = cursor { let cursorOperation = CKQueryOperation(cursor: queryCursor) cursorOperation.zoneID = zone - for try await value in self.query(operation) { + for try await value in self.query(cursorOperation) { switch value { case let .record(record): continuation.yield(record) From 4c1883156f13b74e8ea2752f009a8dc9ee9d2329 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 23 Sep 2022 17:35:28 -0700 Subject: [PATCH 144/229] [App] Moved AppIntents files --- .../{ => AppEntity}/KeyEntity.swift | 40 --------- .../{ => AppEntity}/LockEntity.swift | 47 ---------- .../AppEnum/LockStatusAppEnum.swift | 33 +++++++ .../AppEnum/PermissionAppEnum.swift | 48 ++++++++++ .../AppEnum/UnlockActionAppEnum.swift | 33 +++++++ .../{ => AppIntent}/ScanLocksIntent.swift | 0 .../{ => AppIntent}/UnlockIntent.swift | 0 .../{ => EntityQuery}/KeyQuery.swift | 0 .../{ => EntityQuery}/LockQuery.swift | 0 Xcode/LockIntents/Shortcuts.swift | 18 +++- Xcode/LockIntents/TestIntent.swift | 89 ------------------- Xcode/SmartLock.xcodeproj/project.pbxproj | 60 ++++++++++--- 12 files changed, 181 insertions(+), 187 deletions(-) rename Xcode/LockIntents/{ => AppEntity}/KeyEntity.swift (55%) rename Xcode/LockIntents/{ => AppEntity}/LockEntity.swift (61%) create mode 100644 Xcode/LockIntents/AppEnum/LockStatusAppEnum.swift create mode 100644 Xcode/LockIntents/AppEnum/PermissionAppEnum.swift create mode 100644 Xcode/LockIntents/AppEnum/UnlockActionAppEnum.swift rename Xcode/LockIntents/{ => AppIntent}/ScanLocksIntent.swift (100%) rename Xcode/LockIntents/{ => AppIntent}/UnlockIntent.swift (100%) rename Xcode/LockIntents/{ => EntityQuery}/KeyQuery.swift (100%) rename Xcode/LockIntents/{ => EntityQuery}/LockQuery.swift (100%) delete mode 100644 Xcode/LockIntents/TestIntent.swift diff --git a/Xcode/LockIntents/KeyEntity.swift b/Xcode/LockIntents/AppEntity/KeyEntity.swift similarity index 55% rename from Xcode/LockIntents/KeyEntity.swift rename to Xcode/LockIntents/AppEntity/KeyEntity.swift index cf22ffd7..2ba13f26 100644 --- a/Xcode/LockIntents/KeyEntity.swift +++ b/Xcode/LockIntents/AppEntity/KeyEntity.swift @@ -56,43 +56,3 @@ extension KeyEntity { self.permission = .init(rawValue: key.permission.type.rawValue)! } } - -// MARK: - Supporting Types - -@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) -extension KeyEntity { - - enum Permission: UInt8, AppEnum { - - case owner = 0x00 - case admin = 0x01 - case anytime = 0x02 - case scheduled = 0x03 - - static var typeDisplayRepresentation: TypeDisplayRepresentation { - "Permission" - } - - static var caseDisplayRepresentations: [Permission : DisplayRepresentation] { - [ - .owner: "Owner", - .admin: "Admin", - .anytime: "Anytime", - .scheduled: "Scheduled" - ] - } - - var imageName: String { - switch self { - case .owner: - return "permissionOwner" - case .admin: - return "permissionAdmin" - case .anytime: - return "permissionAnytime" - case .scheduled: - return "permissionScheduled" - } - } - } -} diff --git a/Xcode/LockIntents/LockEntity.swift b/Xcode/LockIntents/AppEntity/LockEntity.swift similarity index 61% rename from Xcode/LockIntents/LockEntity.swift rename to Xcode/LockIntents/AppEntity/LockEntity.swift index a31d147c..568a5f43 100644 --- a/Xcode/LockIntents/LockEntity.swift +++ b/Xcode/LockIntents/AppEntity/LockEntity.swift @@ -73,50 +73,3 @@ extension LockEntity { self.key = key } } - -// MARK: - Supporting Typess= - -@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) -extension LockEntity { - - enum LockStatus: UInt8, AppEnum { - - /// Initial Status - case setup = 0x00 - - /// Idle / Unlock Mode - case unlock = 0x01 - - static var typeDisplayRepresentation: TypeDisplayRepresentation { - "Lock Status" - } - - static var caseDisplayRepresentations: [LockStatus : DisplayRepresentation] { - [ - .setup: "Needs Setup", - .unlock: "Ready to Unlock" - ] - } - } - - @available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) - enum UnlockAction: UInt8, AppEnum { - - /// Unlock immediately. - case `default` = 0b01 - - /// Unlock when button is pressed. - case button = 0b10 - - static var typeDisplayRepresentation: TypeDisplayRepresentation { - "Unlock Action" - } - - static var caseDisplayRepresentations: [UnlockAction : DisplayRepresentation] { - [ - .default: "Default", - .button: "Button" - ] - } - } -} diff --git a/Xcode/LockIntents/AppEnum/LockStatusAppEnum.swift b/Xcode/LockIntents/AppEnum/LockStatusAppEnum.swift new file mode 100644 index 00000000..a38afedf --- /dev/null +++ b/Xcode/LockIntents/AppEnum/LockStatusAppEnum.swift @@ -0,0 +1,33 @@ +// +// LockStatus.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/23/22. +// + +import Foundation +import AppIntents + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +extension LockEntity { + + enum LockStatus: UInt8, AppEnum { + + /// Initial Status + case setup = 0x00 + + /// Idle / Unlock Mode + case unlock = 0x01 + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Lock Status" + } + + static var caseDisplayRepresentations: [LockStatus : DisplayRepresentation] { + [ + .setup: "Needs Setup", + .unlock: "Ready to Unlock" + ] + } + } +} diff --git a/Xcode/LockIntents/AppEnum/PermissionAppEnum.swift b/Xcode/LockIntents/AppEnum/PermissionAppEnum.swift new file mode 100644 index 00000000..9719e6a1 --- /dev/null +++ b/Xcode/LockIntents/AppEnum/PermissionAppEnum.swift @@ -0,0 +1,48 @@ +// +// PermissionAppEnum.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/23/22. +// + +import Foundation +import AppIntents + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +extension KeyEntity { + + enum Permission: UInt8, AppEnum { + + case owner = 0x00 + case admin = 0x01 + case anytime = 0x02 + case scheduled = 0x03 + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Permission" + } + + static var caseDisplayRepresentations: [Permission : DisplayRepresentation] { + [ + .owner: "Owner", + .admin: "Admin", + .anytime: "Anytime", + .scheduled: "Scheduled" + ] + } + + var imageName: String { + switch self { + case .owner: + return "permissionOwner" + case .admin: + return "permissionAdmin" + case .anytime: + return "permissionAnytime" + case .scheduled: + return "permissionScheduled" + } + } + } +} + diff --git a/Xcode/LockIntents/AppEnum/UnlockActionAppEnum.swift b/Xcode/LockIntents/AppEnum/UnlockActionAppEnum.swift new file mode 100644 index 00000000..90db09ac --- /dev/null +++ b/Xcode/LockIntents/AppEnum/UnlockActionAppEnum.swift @@ -0,0 +1,33 @@ +// +// UnlockAction.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/23/22. +// + +import Foundation +import AppIntents + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +extension LockEntity { + + enum UnlockAction: UInt8, AppEnum { + + /// Unlock immediately. + case `default` = 0b01 + + /// Unlock when button is pressed. + case button = 0b10 + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Unlock Action" + } + + static var caseDisplayRepresentations: [UnlockAction : DisplayRepresentation] { + [ + .default: "Default", + .button: "Button" + ] + } + } +} diff --git a/Xcode/LockIntents/ScanLocksIntent.swift b/Xcode/LockIntents/AppIntent/ScanLocksIntent.swift similarity index 100% rename from Xcode/LockIntents/ScanLocksIntent.swift rename to Xcode/LockIntents/AppIntent/ScanLocksIntent.swift diff --git a/Xcode/LockIntents/UnlockIntent.swift b/Xcode/LockIntents/AppIntent/UnlockIntent.swift similarity index 100% rename from Xcode/LockIntents/UnlockIntent.swift rename to Xcode/LockIntents/AppIntent/UnlockIntent.swift diff --git a/Xcode/LockIntents/KeyQuery.swift b/Xcode/LockIntents/EntityQuery/KeyQuery.swift similarity index 100% rename from Xcode/LockIntents/KeyQuery.swift rename to Xcode/LockIntents/EntityQuery/KeyQuery.swift diff --git a/Xcode/LockIntents/LockQuery.swift b/Xcode/LockIntents/EntityQuery/LockQuery.swift similarity index 100% rename from Xcode/LockIntents/LockQuery.swift rename to Xcode/LockIntents/EntityQuery/LockQuery.swift diff --git a/Xcode/LockIntents/Shortcuts.swift b/Xcode/LockIntents/Shortcuts.swift index 6bb2d6b8..6f361ff2 100644 --- a/Xcode/LockIntents/Shortcuts.swift +++ b/Xcode/LockIntents/Shortcuts.swift @@ -9,14 +9,30 @@ import AppIntents @available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) struct AppShortcuts: AppShortcutsProvider { + + /// The background color of the tile that Shortcuts displays for each of the app's App Shortcuts. + static var shortcutTileColor: ShortcutTileColor { + .navy + } static var appShortcuts: [AppShortcut] { + + // Scan AppShortcut( intent: ScanLocksIntent(), phrases: [ "Scan for locks with \(.applicationName)", ], - systemImageName: "lock" + systemImageName: "arrow.clockwise" + ) + + // Unlock + AppShortcut( + intent: UnlockIntent(), + phrases: [ + "Unlock my door with \(.applicationName)", + ], + systemImageName: "lock.open.fill" ) } } diff --git a/Xcode/LockIntents/TestIntent.swift b/Xcode/LockIntents/TestIntent.swift deleted file mode 100644 index faedb6d9..00000000 --- a/Xcode/LockIntents/TestIntent.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// TestIntent.swift -// LockIntents -// -// Created by Alsey Coleman Miller on 9/23/22. -// - -import AppIntents - -#if DEBUG0 -struct TestIntents: AppIntent { - - static var title: LocalizedStringResource = "TestIntents" - - @Parameter(title: "Duration", default: 2) - var duration: TimeInterval - - func perform() async throws -> some IntentResult { - try await Task.sleep(for: .seconds(duration)) - let entity = { TestLockEntity(id: UUID(), buildVersion: 1, version: "1.0.0") } - return .result(value: [entity(), entity(), entity()]) - } -} - -struct TestIntents2: AppIntent { - - static var title: LocalizedStringResource = "TestIntents 2" - - @Parameter(title: "Lock") - var lock: TestLockEntity - - func perform() async throws -> some IntentResult { - try await Task.sleep(for: .seconds(1)) - let entity = { TestLockEntity(id: UUID(), buildVersion: 1, version: "1.0.0") } - return .result(value: entity().id.description) - } -} - - -/// Lock Intent Entity -struct TestLockEntity: AppEntity, Identifiable, Sendable { - - let id: UUID - - /// Firmware build number - var buildVersion: UInt64 - - /// Firmware version - var version: String - - /// Device state - //var status: CoreLock.LockStatus - - /// Supported lock actions - //var unlockActions: Set -} - -extension TestLockEntity { - - static var defaultQuery = TestLockQuery() - - static var typeDisplayRepresentation: TypeDisplayRepresentation { - "Lock" - } - - var displayRepresentation: DisplayRepresentation { - DisplayRepresentation( - title: "Lock", - subtitle: "UUID \(id.description) v\(version.description)", - image: .init(systemName: "lock.fill") - ) - } - - func suggestedEntities() async throws -> [TestLockEntity] { - let entity = { TestLockEntity(id: UUID(), buildVersion: 1, version: "1.0.0") } - return [entity(), entity(), entity()] - } -} - -struct TestLockQuery: EntityQuery { - - func entities(for identifiers: [UUID]) async throws -> [TestLockEntity] { - return identifiers.map { - TestLockEntity(id: $0, buildVersion: 1, version: "1") - } - } -} - -#endif diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 7f25a2ed..ea261978 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -88,12 +88,14 @@ 6E84E52B28DDC841008CAE85 /* KeyEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E52528DDB0F2008CAE85 /* KeyEntity.swift */; }; 6E84E52C28DDC841008CAE85 /* LockQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFD28DD491100F03735 /* LockQuery.swift */; }; 6E84E52D28DDC841008CAE85 /* LockEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFB28DD438500F03735 /* LockEntity.swift */; }; - 6E84E52E28DDC90B008CAE85 /* TestIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E52128DD9713008CAE85 /* TestIntent.swift */; }; 6E84E53028DDCF4F008CAE85 /* KeyQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E52F28DDCF4F008CAE85 /* KeyQuery.swift */; }; 6E84E53228DDDC11008CAE85 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6E84E53128DDDC11008CAE85 /* Assets.xcassets */; }; 6E84E53428DDDCDC008CAE85 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E53328DDDCDC008CAE85 /* Bundle.swift */; }; 6E84E53628DDE01F008CAE85 /* AssetExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E53528DDE01F008CAE85 /* AssetExtractor.swift */; }; 6E84E53828DDE08A008CAE85 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E53728DDE08A008CAE85 /* UIImage.swift */; }; + 6E84E53F28DE86A1008CAE85 /* LockStatusAppEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E53E28DE86A1008CAE85 /* LockStatusAppEnum.swift */; }; + 6E84E54128DE8728008CAE85 /* UnlockActionAppEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E54028DE8728008CAE85 /* UnlockActionAppEnum.swift */; }; + 6E84E54328DE878D008CAE85 /* PermissionAppEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E54228DE878D008CAE85 /* PermissionAppEnum.swift */; }; 6E8BBFDA28DC2CA000F03735 /* SetupLockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */; }; 6E8BBFDC28DCC8F300F03735 /* AsyncFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */; }; 6E8BBFEB28DD301B00F03735 /* LockIntentsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFEA28DD301B00F03735 /* LockIntentsExtension.swift */; }; @@ -217,13 +219,15 @@ 6E6A97F028DBEE0700C689F6 /* Predicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Predicate.swift; sourceTree = ""; }; 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyInvitationView.swift; sourceTree = ""; }; 6E84E51F28DD9646008CAE85 /* Shortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shortcuts.swift; sourceTree = ""; }; - 6E84E52128DD9713008CAE85 /* TestIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestIntent.swift; sourceTree = ""; }; 6E84E52528DDB0F2008CAE85 /* KeyEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyEntity.swift; sourceTree = ""; }; 6E84E52F28DDCF4F008CAE85 /* KeyQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyQuery.swift; sourceTree = ""; }; 6E84E53128DDDC11008CAE85 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 6E84E53328DDDCDC008CAE85 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; 6E84E53528DDE01F008CAE85 /* AssetExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExtractor.swift; sourceTree = ""; }; 6E84E53728DDE08A008CAE85 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; + 6E84E53E28DE86A1008CAE85 /* LockStatusAppEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockStatusAppEnum.swift; sourceTree = ""; }; + 6E84E54028DE8728008CAE85 /* UnlockActionAppEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockActionAppEnum.swift; sourceTree = ""; }; + 6E84E54228DE878D008CAE85 /* PermissionAppEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionAppEnum.swift; sourceTree = ""; }; 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupLockView.swift; sourceTree = ""; }; 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncFetchRequest.swift; sourceTree = ""; }; 6E8BBFE828DD301B00F03735 /* LockIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.extensionkit-extension"; includeInIndex = 0; path = LockIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -448,19 +452,53 @@ path = AppKit; sourceTree = ""; }; - 6E8BBFE928DD301B00F03735 /* LockIntents */ = { + 6E84E53A28DE862B008CAE85 /* AppEntity */ = { isa = PBXGroup; children = ( - 6E8BC00A28DD54E200F03735 /* LockIntents.entitlements */, - 6E8BBFEA28DD301B00F03735 /* LockIntentsExtension.swift */, - 6E8BBFEC28DD301B00F03735 /* ScanLocksIntent.swift */, - 6E84E51F28DD9646008CAE85 /* Shortcuts.swift */, - 6E8BBFFF28DD492300F03735 /* UnlockIntent.swift */, 6E8BBFFB28DD438500F03735 /* LockEntity.swift */, 6E84E52528DDB0F2008CAE85 /* KeyEntity.swift */, + ); + path = AppEntity; + sourceTree = ""; + }; + 6E84E53B28DE8643008CAE85 /* AppIntent */ = { + isa = PBXGroup; + children = ( + 6E8BBFEC28DD301B00F03735 /* ScanLocksIntent.swift */, + 6E8BBFFF28DD492300F03735 /* UnlockIntent.swift */, + ); + path = AppIntent; + sourceTree = ""; + }; + 6E84E53C28DE8658008CAE85 /* EntityQuery */ = { + isa = PBXGroup; + children = ( 6E8BBFFD28DD491100F03735 /* LockQuery.swift */, 6E84E52F28DDCF4F008CAE85 /* KeyQuery.swift */, - 6E84E52128DD9713008CAE85 /* TestIntent.swift */, + ); + path = EntityQuery; + sourceTree = ""; + }; + 6E84E53D28DE8679008CAE85 /* AppEnum */ = { + isa = PBXGroup; + children = ( + 6E84E53E28DE86A1008CAE85 /* LockStatusAppEnum.swift */, + 6E84E54028DE8728008CAE85 /* UnlockActionAppEnum.swift */, + 6E84E54228DE878D008CAE85 /* PermissionAppEnum.swift */, + ); + path = AppEnum; + sourceTree = ""; + }; + 6E8BBFE928DD301B00F03735 /* LockIntents */ = { + isa = PBXGroup; + children = ( + 6E8BC00A28DD54E200F03735 /* LockIntents.entitlements */, + 6E8BBFEA28DD301B00F03735 /* LockIntentsExtension.swift */, + 6E84E51F28DD9646008CAE85 /* Shortcuts.swift */, + 6E84E53D28DE8679008CAE85 /* AppEnum */, + 6E84E53B28DE8643008CAE85 /* AppIntent */, + 6E84E53A28DE862B008CAE85 /* AppEntity */, + 6E84E53C28DE8658008CAE85 /* EntityQuery */, 6E8BBFEE28DD301B00F03735 /* Info.plist */, ); path = LockIntents; @@ -729,7 +767,6 @@ buildActionMask = 2147483647; files = ( 6E8BBFEB28DD301B00F03735 /* LockIntentsExtension.swift in Sources */, - 6E84E52E28DDC90B008CAE85 /* TestIntent.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -741,6 +778,7 @@ 6E84E52D28DDC841008CAE85 /* LockEntity.swift in Sources */, 6E84E52728DDC841008CAE85 /* ScanLocksIntent.swift in Sources */, 6E84E52A28DDC841008CAE85 /* Shortcuts.swift in Sources */, + 6E84E54328DE878D008CAE85 /* PermissionAppEnum.swift in Sources */, 6E84E52B28DDC841008CAE85 /* KeyEntity.swift in Sources */, 6E4CB62528D792BF00116573 /* InfoPlist.swift in Sources */, 6E21831D28D834D200A622B3 /* ContentView.swift in Sources */, @@ -750,9 +788,11 @@ 6E21834C28D9140D00A622B3 /* KeysView.swift in Sources */, 6E84E52828DDC841008CAE85 /* UnlockIntent.swift in Sources */, 6E21831B28D8341000A622B3 /* SidebarView.swift in Sources */, + 6E84E53F28DE86A1008CAE85 /* LockStatusAppEnum.swift in Sources */, 6E3276D028D70C9400AF171B /* NearbyDevicesView.swift in Sources */, 6E84E52C28DDC841008CAE85 /* LockQuery.swift in Sources */, 6E21831F28D84A7300A622B3 /* SidebarLabel.swift in Sources */, + 6E84E54128DE8728008CAE85 /* UnlockActionAppEnum.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 72f502de8b42829db29c17773873b74262c28629 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 23 Sep 2022 19:36:30 -0700 Subject: [PATCH 145/229] [App] Added `FetchEventsIntent` --- Sources/CoreLock/EventStore.swift | 3 + .../AppIntent/FetchEventsIntent.swift | 105 ++++++++++++++++++ .../AppIntent/ScanLocksIntent.swift | 8 +- .../LockIntents/AppIntent/UnlockIntent.swift | 36 +----- Xcode/LockKit/Model/Store.swift | 16 ++- Xcode/LockKit/View/EventsView.swift | 2 + Xcode/SmartLock.xcodeproj/project.pbxproj | 4 + 7 files changed, 127 insertions(+), 47 deletions(-) create mode 100644 Xcode/LockIntents/AppIntent/FetchEventsIntent.swift diff --git a/Sources/CoreLock/EventStore.swift b/Sources/CoreLock/EventStore.swift index 7c8b19d9..e0b04d34 100644 --- a/Sources/CoreLock/EventStore.swift +++ b/Sources/CoreLock/EventStore.swift @@ -57,10 +57,13 @@ public extension LockEvent { } } + /// Lock Event Fetch Request Predicate struct Predicate: Codable, Equatable, Hashable { + /// The keys used to filter by the results. public var keys: [UUID]? + /// The start date to filter results. public var start: Date? public var end: Date? diff --git a/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift b/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift new file mode 100644 index 00000000..e473e412 --- /dev/null +++ b/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift @@ -0,0 +1,105 @@ +// +// FetchEventsIntent.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/23/22. +// + +import AppIntents +import SwiftUI +import LockKit + +/// Intent for fetching events +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +struct FetchEventsIntent: AppIntent { + + static var title: LocalizedStringResource { "Fetch Events" } + + static var description: IntentDescription { + IntentDescription( + "Fetch the events for a specified lock.", + categoryName: "Utility", + searchKeywords: ["events", "bluetooth", "lock"] + ) + } + + static var parameterSummary: some ParameterSummary { + Summary("Fetch the events for \(\.$lock)") + } + + /// The specified lock to unlock. + @Parameter( + title: "Lock", + description: "The specified lock to fetch events from." + ) + var lock: LockEntity + + /// The fetch offset of the fetch request. + @Parameter( + title: "Offset", + description: "The fetch offset of the fetch request.", + default: 0 + ) + var offset: Int + + /// The fetch limit of the fetch request. + @Parameter( + title: "Limit", + description: "The fetch limit of the fetch request." + ) + var limit: Int? + + /// The keys used to filter the results. + @Parameter( + title: "Keys", + description: "The keys used to filter the results.", + default: [] + ) + var keys: [KeyEntity] + + /// The start date to filter results. + @Parameter( + title: "Start", + description: "The start date to filter results." + ) + var start: Date? + + /// The end date to filter results. + @Parameter( + title: "End", + description: "The end date to filter results." + ) + var end: Date? + + @MainActor + func perform() async throws -> some IntentResult { + let store = Store.shared + let fetchRequest = LockEvent.FetchRequest( + offset: UInt8(offset), + limit: limit.flatMap(UInt8.init), + predicate: .init( + keys: keys.map { $0.id }, + start: start, + end: end + ) + ) + // search for lock if not in cache + guard let peripheral = try await store.device(for: lock.id) else { + throw LockError.notInRange(lock: lock.id) + } + // fetch events + let events = try await Store.shared.listEvents( + for: peripheral, + fetchRequest: fetchRequest + ) + return .result( + value: "\(events)" + ) + } +} + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +extension FetchEventsIntent { + + +} diff --git a/Xcode/LockIntents/AppIntent/ScanLocksIntent.swift b/Xcode/LockIntents/AppIntent/ScanLocksIntent.swift index 7dc19850..f35e4577 100644 --- a/Xcode/LockIntents/AppIntent/ScanLocksIntent.swift +++ b/Xcode/LockIntents/AppIntent/ScanLocksIntent.swift @@ -36,13 +36,7 @@ struct ScanLocksIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { let store = Store.shared - do { try await store.central.waitPowerOn() } - catch { - return .result( - value: [LockEntity](), - view: view(for: []) - ) - } + try await store.central.waitPowerOn() try await store.scan(duration: duration) let locks = store.lockInformation .sorted(by: { $0.key.id.description < $1.key.id.description }) diff --git a/Xcode/LockIntents/AppIntent/UnlockIntent.swift b/Xcode/LockIntents/AppIntent/UnlockIntent.swift index 22dff073..5a4aa632 100644 --- a/Xcode/LockIntents/AppIntent/UnlockIntent.swift +++ b/Xcode/LockIntents/AppIntent/UnlockIntent.swift @@ -34,39 +34,7 @@ struct UnlockIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { - do { - try await Store.shared.unlock(for: lock.id) - } - catch { - return .result( - value: false, - content: { ResultView(error: error.localizedDescription) } - ) - } - return .result( - value: true, - content: { ResultView(error: nil) } - ) - } -} - - -@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) -extension UnlockIntent { - - struct ResultView: View { - - let error: String? - - var body: some View { - VStack(alignment: .center, spacing: 8) { - if let error = error { - Text("Unable to unlock") - Text(verbatim: error) - } else { - Text("Unlocked") - } - } - } + try await Store.shared.unlock(for: lock.id) + return .result() } } diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 9a0ad9a5..34de1d14 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -748,20 +748,22 @@ public extension Store { log("Recieved \(keys.count) keys and \(newKeys.count) pending keys for lock \(information.id)") } + @discardableResult func listEvents( for peripheral: NativeCentral.Peripheral, fetchRequest: LockEvent.FetchRequest? = nil - ) async throws { + ) async throws -> [LockEvent] { stopScanning() - try await central.connection(for: peripheral) { + return try await central.connection(for: peripheral) { try await self.listEvents(for: $0, fetchRequest: fetchRequest) } } + @discardableResult func listEvents( for connection: GATTConnection, fetchRequest: LockEvent.FetchRequest? = nil - ) async throws { + ) async throws -> [LockEvent] { let peripheral = connection.peripheral // get lock key guard let information = self.lockInformation[peripheral] else { @@ -778,14 +780,15 @@ public extension Store { ) let context = backgroundContext - var eventsCount = 0 + var events = [LockEvent]() + events.reserveCapacity(Int(fetchRequest?.limit ?? 10)) // BLE request let centralLog = central.log let stream = try await connection.listEvents(fetchRequest: fetchRequest, using: key, log: centralLog) for try await notification in stream { if let event = notification.event { centralLog?("Recieved \(event.type) event \(event.id)") - eventsCount += 1 + events.append(event) // store in CoreData await context.commit { (context) in try context.insert(event, for: information.id) @@ -803,6 +806,7 @@ public extension Store { objectWillChange.send() - log("Recieved \(eventsCount) events for lock \(information.id)") + log("Recieved \(events.count) events for lock \(information.id)") + return events } } diff --git a/Xcode/LockKit/View/EventsView.swift b/Xcode/LockKit/View/EventsView.swift index 22b677e5..80431a0c 100644 --- a/Xcode/LockKit/View/EventsView.swift +++ b/Xcode/LockKit/View/EventsView.swift @@ -18,6 +18,8 @@ public struct EventsView: View { @Environment(\.managedObjectContext) public var managedObjectContext + // FIXME: Filter by lock as well + @State public var predicate: LockEvent.Predicate? diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index ea261978..4fdd444f 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -96,6 +96,7 @@ 6E84E53F28DE86A1008CAE85 /* LockStatusAppEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E53E28DE86A1008CAE85 /* LockStatusAppEnum.swift */; }; 6E84E54128DE8728008CAE85 /* UnlockActionAppEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E54028DE8728008CAE85 /* UnlockActionAppEnum.swift */; }; 6E84E54328DE878D008CAE85 /* PermissionAppEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E54228DE878D008CAE85 /* PermissionAppEnum.swift */; }; + 6E84E54628DEA0CE008CAE85 /* FetchEventsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E54428DE89BC008CAE85 /* FetchEventsIntent.swift */; }; 6E8BBFDA28DC2CA000F03735 /* SetupLockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */; }; 6E8BBFDC28DCC8F300F03735 /* AsyncFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */; }; 6E8BBFEB28DD301B00F03735 /* LockIntentsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFEA28DD301B00F03735 /* LockIntentsExtension.swift */; }; @@ -228,6 +229,7 @@ 6E84E53E28DE86A1008CAE85 /* LockStatusAppEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockStatusAppEnum.swift; sourceTree = ""; }; 6E84E54028DE8728008CAE85 /* UnlockActionAppEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockActionAppEnum.swift; sourceTree = ""; }; 6E84E54228DE878D008CAE85 /* PermissionAppEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionAppEnum.swift; sourceTree = ""; }; + 6E84E54428DE89BC008CAE85 /* FetchEventsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchEventsIntent.swift; sourceTree = ""; }; 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupLockView.swift; sourceTree = ""; }; 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncFetchRequest.swift; sourceTree = ""; }; 6E8BBFE828DD301B00F03735 /* LockIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.extensionkit-extension"; includeInIndex = 0; path = LockIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -466,6 +468,7 @@ children = ( 6E8BBFEC28DD301B00F03735 /* ScanLocksIntent.swift */, 6E8BBFFF28DD492300F03735 /* UnlockIntent.swift */, + 6E84E54428DE89BC008CAE85 /* FetchEventsIntent.swift */, ); path = AppIntent; sourceTree = ""; @@ -777,6 +780,7 @@ 6E2182F128D7B4FA00A622B3 /* SettingsView.swift in Sources */, 6E84E52D28DDC841008CAE85 /* LockEntity.swift in Sources */, 6E84E52728DDC841008CAE85 /* ScanLocksIntent.swift in Sources */, + 6E84E54628DEA0CE008CAE85 /* FetchEventsIntent.swift in Sources */, 6E84E52A28DDC841008CAE85 /* Shortcuts.swift in Sources */, 6E84E54328DE878D008CAE85 /* PermissionAppEnum.swift in Sources */, 6E84E52B28DDC841008CAE85 /* KeyEntity.swift in Sources */, From 628150daa604abb24c801f5aaca5cd8006909891 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 24 Sep 2022 09:20:56 -0700 Subject: [PATCH 146/229] [App] Added mock types --- .../AppIntent/ScanLocksIntent.swift | 2 +- .../LockIntents/AppIntent/UnlockIntent.swift | 3 +- Xcode/LockKit/Model/Central.swift | 46 +- .../Model/Mock/MockAdvertisement.swift | 86 ++++ Xcode/LockKit/Model/Mock/MockAttributes.swift | 127 ++++++ Xcode/LockKit/Model/Mock/MockCentral.swift | 406 ++++++++++++++++++ Xcode/LockKit/Model/Mock/MockLock.swift | 39 ++ Xcode/LockKit/Model/Mock/MockScanData.swift | 79 ++++ Xcode/LockKit/Model/Store.swift | 12 +- Xcode/SmartLock.xcodeproj/project.pbxproj | 28 ++ Xcode/SmartLock/View/NearbyDevicesView.swift | 10 +- 11 files changed, 819 insertions(+), 19 deletions(-) create mode 100644 Xcode/LockKit/Model/Mock/MockAdvertisement.swift create mode 100644 Xcode/LockKit/Model/Mock/MockAttributes.swift create mode 100644 Xcode/LockKit/Model/Mock/MockCentral.swift create mode 100644 Xcode/LockKit/Model/Mock/MockLock.swift create mode 100644 Xcode/LockKit/Model/Mock/MockScanData.swift diff --git a/Xcode/LockIntents/AppIntent/ScanLocksIntent.swift b/Xcode/LockIntents/AppIntent/ScanLocksIntent.swift index f35e4577..01084ba7 100644 --- a/Xcode/LockIntents/AppIntent/ScanLocksIntent.swift +++ b/Xcode/LockIntents/AppIntent/ScanLocksIntent.swift @@ -36,7 +36,7 @@ struct ScanLocksIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { let store = Store.shared - try await store.central.waitPowerOn() + try await store.central.wait(for: .poweredOn) try await store.scan(duration: duration) let locks = store.lockInformation .sorted(by: { $0.key.id.description < $1.key.id.description }) diff --git a/Xcode/LockIntents/AppIntent/UnlockIntent.swift b/Xcode/LockIntents/AppIntent/UnlockIntent.swift index 5a4aa632..02c192de 100644 --- a/Xcode/LockIntents/AppIntent/UnlockIntent.swift +++ b/Xcode/LockIntents/AppIntent/UnlockIntent.swift @@ -8,7 +8,7 @@ import AppIntents import SwiftUI import LockKit - +/* @available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) struct UnlockIntent: AppIntent { @@ -38,3 +38,4 @@ struct UnlockIntent: AppIntent { return .result() } } +*/ diff --git a/Xcode/LockKit/Model/Central.swift b/Xcode/LockKit/Model/Central.swift index 942e99a9..5fda54be 100644 --- a/Xcode/LockKit/Model/Central.swift +++ b/Xcode/LockKit/Model/Central.swift @@ -5,23 +5,59 @@ // Created by Alsey Coleman Miller on 9/18/22. // -#if canImport(CoreBluetooth) && canImport(DarwinGATT) import Foundation import CoreBluetooth import Bluetooth import GATT import DarwinGATT +#if targetEnvironment(simulator) + +public typealias NativeCentral = MockCentral +public typealias NativePeripheral = MockCentral.Peripheral + +public extension NativeCentral { + + private struct Cache { + static let central = MockCentral() + } + + static var shared: NativeCentral { + return Cache.central + } +} + +#else + public typealias NativeCentral = DarwinCentral public typealias NativePeripheral = DarwinCentral.Peripheral -public extension DarwinCentral { +public extension NativeCentral { + + private struct Cache { + static let central = DarwinCentral( + options: .init(showPowerAlert: true) + ) + } + + static var shared: NativeCentral { + return Cache.central + } +} + +#endif + +public extension NativeCentral { /// Wait for CoreBluetooth to be ready. - func waitPowerOn(warning: Int = 3, timeout: Int = 10) async throws { + func wait( + for state: DarwinBluetoothState, + warning: Int = 3, + timeout: Int = 10 + ) async throws { var powerOnWait = 0 - while await state != .poweredOn { + while await self.state != state { // inform user after 3 seconds if powerOnWait == warning { @@ -35,5 +71,3 @@ public extension DarwinCentral { } } } - -#endif diff --git a/Xcode/LockKit/Model/Mock/MockAdvertisement.swift b/Xcode/LockKit/Model/Mock/MockAdvertisement.swift new file mode 100644 index 00000000..8897a95c --- /dev/null +++ b/Xcode/LockKit/Model/Mock/MockAdvertisement.swift @@ -0,0 +1,86 @@ +// +// MockAdvertisement.swift +// BluetoothExplorer +// +// Created by Alsey Coleman Miller on 31/10/21. +// Copyright © 2021 Alsey Coleman Miller. All rights reserved. +// + +#if DEBUG +import Foundation +import Bluetooth +import GATT + +/// Mock Advertisement Data +public struct MockAdvertisementData: AdvertisementData { + + /// The local name of a peripheral. + public let localName: String? + + /// The Manufacturer data of a peripheral. + public let manufacturerData: ManufacturerSpecificData? + + /// This value is available if the broadcaster (peripheral) provides its Tx power level in its advertising packet. + /// Using the RSSI value and the Tx power level, it is possible to calculate path loss. + public let txPowerLevel: Double? + + /// Service-specific advertisement data. + public let serviceData: [BluetoothUUID: Data]? + + /// An array of service UUIDs + public let serviceUUIDs: [BluetoothUUID]? + + /// An array of one or more `BluetoothUUID`, representing Service UUIDs. + public let solicitedServiceUUIDs: [BluetoothUUID]? + + internal init( + localName: String? = nil, + manufacturerData: ManufacturerSpecificData? = nil, + txPowerLevel: Double? = nil, + serviceData: [BluetoothUUID : Data]? = nil, + serviceUUIDs: [BluetoothUUID]? = nil, + solicitedServiceUUIDs: [BluetoothUUID]? = nil + ) { + + self.localName = localName + self.manufacturerData = manufacturerData + self.txPowerLevel = txPowerLevel + self.serviceData = serviceData + self.serviceUUIDs = serviceUUIDs + self.solicitedServiceUUIDs = solicitedServiceUUIDs + } +} + +public extension MockAdvertisementData { + + static let beacon = MockAdvertisementData( + localName: nil, + manufacturerData: ManufacturerSpecificData(data: Data([0x4c, 0x00, 0x02, 0x15, 0xb9, 0x40, 0x7f, 0x30, 0xf5, 0xf8, 0x46, 0x6e, 0xaf, 0xf9, 0x25, 0x55, 0x6b, 0x57, 0xfe, 0x6d, 0x29, 0x4c, 0x90, 0x39, 0x74])), + txPowerLevel: nil, + serviceData: nil, + serviceUUIDs: nil, + solicitedServiceUUIDs: nil + ) + + static let smartThermostat = MockAdvertisementData( + localName: "CLI-W200", + manufacturerData: ManufacturerSpecificData(data: Data([0xd9, 0x01, 0x01, 0x02, 0x00, 0x00, 0x8c, 0x85, 0x90, 0xcb, 0x31, 0x74, 0x00, 0x60])), + txPowerLevel: nil, + serviceData: nil, + serviceUUIDs: [.savantSystems2], + solicitedServiceUUIDs: nil + ) + + static let lock = MockAdvertisementData( + localName: "Lock", + manufacturerData: nil, + txPowerLevel: nil, + serviceData: nil, + serviceUUIDs: [ + LockService.uuid + ], + solicitedServiceUUIDs: nil + ) +} + +#endif diff --git a/Xcode/LockKit/Model/Mock/MockAttributes.swift b/Xcode/LockKit/Model/Mock/MockAttributes.swift new file mode 100644 index 00000000..244299b6 --- /dev/null +++ b/Xcode/LockKit/Model/Mock/MockAttributes.swift @@ -0,0 +1,127 @@ +// +// MockService.swift +// +// +// Created by Alsey Coleman Miller on 18/12/21. +// + +#if DEBUG +import SwiftUI +import Bluetooth +import GATT + +typealias MockService = GATT.Service +typealias MockCharacteristic = GATT.Characteristic +typealias MockDescriptor = GATT.Descriptor + +extension MockService { + + static var deviceInformation: MockService { + Service( + id: 10, + uuid: .deviceInformation, + peripheral: .beacon + ) + } + + static var battery: MockService { + Service( + id: 20, + uuid: .batteryService, + peripheral: .beacon + ) + } + + static var savantSystems: MockService { + Service( + id: 30, + uuid: .savantSystems2, + peripheral: .smartThermostat + ) + } + + static func lock(_ id: UInt8) -> MockService { + Service( + id: 40, + uuid: LockService.uuid, + peripheral: .lock(id) + ) + } +} + +extension MockCharacteristic { + + static var deviceName: MockCharacteristic { + Characteristic( + id: 11, + uuid: .deviceName, + peripheral: .beacon, + properties: [.read] + ) + } + + static var manufacturerName: MockCharacteristic { + Characteristic( + id: 12, + uuid: .manufacturerNameString, + peripheral: .beacon, + properties: [.read] + ) + } + + static var modelNumber: MockCharacteristic { + Characteristic( + id: 13, + uuid: .modelNumberString, + peripheral: .beacon, + properties: [.read] + ) + } + + static var serialNumber: MockCharacteristic { + Characteristic( + id: 14, + uuid: .serialNumberString, + peripheral: .beacon, + properties: [.read] + ) + } + + static var batteryLevel: MockCharacteristic { + Characteristic( + id: 21, + uuid: .batteryLevel, + peripheral: .beacon, + properties: [.read, .notify] + ) + } + + static let savantTest: MockCharacteristic = Characteristic( + id: 31, + uuid: BluetoothUUID(), + peripheral: .smartThermostat, + properties: [.read, .write, .writeWithoutResponse, .notify] + ) + + static func lockInformation(_ id: UInt8) -> MockCharacteristic { + Characteristic( + id: 41, + uuid: LockInformationCharacteristic.uuid, + peripheral: .lock(id), + properties: [.read] + ) + } +} + +extension MockDescriptor { + + static func clientCharacteristicConfiguration(_ peripheral: Peripheral) -> MockDescriptor { + Descriptor( + id: 99, + uuid: .clientCharacteristicConfiguration, + peripheral: peripheral + ) + } +} + +#endif diff --git a/Xcode/LockKit/Model/Mock/MockCentral.swift b/Xcode/LockKit/Model/Mock/MockCentral.swift new file mode 100644 index 00000000..72e7bc1b --- /dev/null +++ b/Xcode/LockKit/Model/Mock/MockCentral.swift @@ -0,0 +1,406 @@ +// +// MockCentral.swift +// +// +// Created by Alsey Coleman Miller on 22/12/21. +// + +#if DEBUG +import Foundation +import Bluetooth +import GATT +import DarwinGATT + +public final class MockCentral: CentralManager { + + /// Central Peripheral Type + public typealias Peripheral = GATT.Peripheral + + /// Central Advertisement Type + public typealias Advertisement = MockAdvertisementData + + /// Central Attribute ID (Handle) + public typealias AttributeID = UInt16 + + // MARK: - Properties + + public var log: ((String) -> ())? + + public var state: DarwinBluetoothState { + get async { + try? await Task.sleep(timeInterval: 0.1) + return await storage.bluetoothState + } + } + + public var peripherals: Set { + get async { + try? await Task.sleep(timeInterval: 0.1) + return await Set(storage.state.scanData.map { $0.peripheral }) + } + } + + private let storage = Storage() + + // MARK: - Initialization + + internal init() { + Task { + try await Task.sleep(timeInterval: 0.5) + await self.storage.stateDidChange(.poweredOn) + } + } + + // MARK: - Methods + + /// Scans for peripherals that are advertising services. + public func scan(filterDuplicates: Bool = true) -> AsyncCentralScan { + return _scan(filterDuplicates: filterDuplicates, with: []) + } + + /// Scans for peripherals that are advertising services. + public func scan(with services: Set, filterDuplicates: Bool = true) -> AsyncCentralScan { + return _scan(filterDuplicates: filterDuplicates, with: services) + } + + /// Scans for peripherals that are advertising services. + private func _scan(filterDuplicates: Bool, with services: Set) -> AsyncCentralScan { + return AsyncCentralScan { continuation in + let state = await self.state + guard state == .poweredOn else { + throw DarwinCentralError.invalidState(state) + } + try await Task.sleep(timeInterval: 1.0) + for scanData in await self.storage.state.scanData { + // apply filter + if services.isEmpty == false { + let foundServiceUUIDs = scanData.advertisementData.serviceUUIDs ?? [] + guard Set(foundServiceUUIDs.filter({ services.contains($0) })) == services else { + continue + } + } + try await Task.sleep(timeInterval: 0.3) + continuation(scanData) + } + } + } + + /// Connect to the specified device + public func connect(to peripheral: Peripheral) async throws { + let state = await self.state + guard state == .poweredOn else { + throw DarwinCentralError.invalidState(state) + } + let _ = await storage.updateState { + $0.connected.insert(peripheral) + } + } + + /// Disconnect the specified device. + public func disconnect(_ peripheral: Peripheral) { + Task { + await self.storage.updateState { + $0.connected.remove(peripheral) + } + } + } + + /// Disconnect all connected devices. + public func disconnectAll() { + Task { + await storage.updateState { + $0.connected.removeAll() + } + } + } + + /// Discover Services + public func discoverServices( + _ services: Set = [], + for peripheral: Peripheral + ) async throws -> [Service] { + let state = await self.state + guard state == .poweredOn else { + throw DarwinCentralError.invalidState(state) + } + return await storage.state.characteristics + .keys + .lazy + .filter { $0.peripheral == peripheral } + .sorted(by: { $0.id < $1.id }) + } + + public func discoverIncludedServices( + _ services: Set = [], + for service: Service + ) async throws -> [Service] { + let state = await self.state + guard state == .poweredOn else { + throw DarwinCentralError.invalidState(state) + } + return [] + } + + /// Discover Characteristics for service + public func discoverCharacteristics( + _ characteristics: Set = [], + for service: Service + ) async throws -> [Characteristic] { + let state = await self.state + guard state == .poweredOn else { + throw DarwinCentralError.invalidState(state) + } + guard await storage.state.connected.contains(service.peripheral) else { + throw CentralError.disconnected + } + guard let characteristics = await storage.state.characteristics[service] else { + throw CentralError.invalidAttribute(service.uuid) + } + return characteristics + .sorted(by: { $0.id < $1.id }) + } + + /// Read Characteristic Value + public func readValue( + for characteristic: Characteristic + ) async throws -> Data { + guard await storage.state.connected.contains(characteristic.peripheral) else { + throw CentralError.disconnected + } + return await storage.state.characteristicValues[characteristic] ?? Data() + } + + /// Write Characteristic Value + public func writeValue( + _ data: Data, + for characteristic: Characteristic, + withResponse: Bool = true + ) async throws { + let state = await self.state + guard state == .poweredOn else { + throw DarwinCentralError.invalidState(state) + } + guard await storage.state.connected.contains(characteristic.peripheral) else { + throw CentralError.disconnected + } + if withResponse { + guard characteristic.properties.contains(.write) else { + throw CentralError.invalidAttribute(characteristic.uuid) + } + } else { + guard characteristic.properties.contains(.writeWithoutResponse) else { + throw CentralError.invalidAttribute(characteristic.uuid) + } + } + // write + await storage.updateState { + $0.characteristicValues[characteristic] = data + } + } + + /// Discover descriptors + public func discoverDescriptors( + for characteristic: Characteristic + ) async throws -> [Descriptor] { + let state = await self.state + guard state == .poweredOn else { + throw DarwinCentralError.invalidState(state) + } + guard await storage.state.connected.contains(characteristic.peripheral) else { + throw CentralError.disconnected + } + return await storage.state.descriptors[characteristic] ?? [] + } + + /// Read descriptor + public func readValue( + for descriptor: Descriptor + ) async throws -> Data { + let state = await self.state + guard state == .poweredOn else { + throw DarwinCentralError.invalidState(state) + } + guard await storage.state.connected.contains(descriptor.peripheral) else { + throw CentralError.disconnected + } + return await storage.state.descriptorValues[descriptor] ?? Data() + } + + /// Write descriptor + public func writeValue( + _ data: Data, + for descriptor: Descriptor + ) async throws { + let state = await self.state + guard state == .poweredOn else { + throw DarwinCentralError.invalidState(state) + } + guard await storage.state.connected.contains(descriptor.peripheral) else { + throw CentralError.disconnected + } + await storage.updateState { + $0.descriptorValues[descriptor] = data + } + } + + public func notify( + for characteristic: GATT.Characteristic + ) async throws -> AsyncCentralNotifications { + let state = await self.state + guard state == .poweredOn else { + throw DarwinCentralError.invalidState(state) + } + guard await storage.state.connected.contains(characteristic.peripheral) else { + throw CentralError.disconnected + } + return AsyncCentralNotifications { [unowned self] continuation in + if let notifications = await storage.state.notifications[characteristic] { + for notification in notifications { + try await Task.sleep(nanoseconds: 100_000_000) + continuation(notification) + } + } + } + } + + /// Read MTU + public func maximumTransmissionUnit(for peripheral: Peripheral) async throws -> MaximumTransmissionUnit { + let state = await self.state + guard state == .poweredOn else { + throw DarwinCentralError.invalidState(state) + } + guard await storage.state.connected.contains(peripheral) else { + throw CentralError.disconnected + } + return .default + } + + // Read RSSI + public func rssi(for peripheral: Peripheral) async throws -> RSSI { + let state = await self.state + guard state == .poweredOn else { + throw DarwinCentralError.invalidState(state) + } + return .init(rawValue: 127)! + } +} + +// MARK: - Supporting Types + +internal extension MockCentral { + + actor Storage { + init() { } + var bluetoothState: DarwinBluetoothState = .unknown + + func stateDidChange(_ newValue: DarwinBluetoothState) { + bluetoothState = newValue + } + + var state = State() + + func updateState(_ block: (inout State) -> (T)) -> T { + return block(&state) + } + + var continuation = Continuation() + + func continuation(_ block: (inout Continuation) -> ()) { + block(&continuation) + } + } +} + +internal extension MockCentral { + + struct State { + var isScanning = false + var scanData: [MockScanData] = [.beacon, .smartThermostat, .lock, .lock(2), .lock(3)] + var connected = Set() + var characteristics: [MockService: [MockCharacteristic]] = [ + .deviceInformation: [ + .deviceName, + .manufacturerName, + .modelNumber, + .serialNumber + ], + .battery: [ + .batteryLevel + ], + .savantSystems: [ + .savantTest + ], + .lock(0x01): [ + .lockInformation(0x01) + ], + .lock(0x02): [ + .lockInformation(0x02) + ], + .lock(0x03): [ + .lockInformation(0x03) + ] + ] + var descriptors: [MockCharacteristic: [MockDescriptor]] = [ + .batteryLevel: [.clientCharacteristicConfiguration(.beacon)], + .savantTest: [.clientCharacteristicConfiguration(.smartThermostat)], + ] + var characteristicValues: [MockCharacteristic: Data] = [ + .deviceName: Data("iBeacon".utf8), + .manufacturerName: Data("Apple Inc.".utf8), + .modelNumber: Data("iPhone11.8".utf8), + .serialNumber: Data(UUID().uuidString.utf8), + .batteryLevel: Data([100]), + .savantTest: Data(UUID().uuidString.utf8), + .lockInformation(0x01): LockInformationCharacteristic( + id: UUID(), + buildVersion: .current, + version: .current, + status: .unlock, + unlockActions: [.default] + ).data, + .lockInformation(0x02): LockInformationCharacteristic( + id: UUID(), + buildVersion: .current, + version: .current, + status: .unlock, + unlockActions: [.default] + ).data, + .lockInformation(0x03): LockInformationCharacteristic( + id: UUID(), + buildVersion: .current, + version: .current, + status: .unlock, + unlockActions: [.default] + ).data + ] + var descriptorValues: [MockDescriptor: Data] = [ + .clientCharacteristicConfiguration(.beacon): Data([0x00]), + .clientCharacteristicConfiguration(.smartThermostat): Data([0x00]), + ] + var notifications: [MockCharacteristic: [Data]] = [ + .batteryLevel: [ + Data([99]), + Data([98]), + Data([95]), + Data([80]), + Data([75]), + Data([25]), + Data([20]), + Data([5]), + Data([1]), + ], + .savantTest: [ + Data(UUID().uuidString.utf8), + Data(UUID().uuidString.utf8), + Data(UUID().uuidString.utf8), + Data(UUID().uuidString.utf8), + ] + ] + } + + struct Continuation { + var scan: AsyncThrowingStream, Error>.Continuation? + var isScanning: AsyncStream.Continuation? + } +} +#endif diff --git a/Xcode/LockKit/Model/Mock/MockLock.swift b/Xcode/LockKit/Model/Mock/MockLock.swift new file mode 100644 index 00000000..97a9d426 --- /dev/null +++ b/Xcode/LockKit/Model/Mock/MockLock.swift @@ -0,0 +1,39 @@ +// +// MockLock.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/24/22. +// + +import Foundation +import CoreLock + +public struct MockLock: Equatable, Hashable, Codable, Identifiable { + + public let id: UUID + + public var status: LockStatus + + public var sharedSecret: KeyData +} + +public extension MockLock { + + static var locks: [MockLock] = [ + MockLock( + id: UUID(uuidString: "669A06D7-5AE5-431B-971C-7A118E77CA51")!, + status: .setup, + sharedSecret: KeyData() + ), + MockLock( + id: UUID(uuidString: "CCAB00A4-A0BE-4D43-B0D6-A9BAB4628256")!, + status: .unlock, + sharedSecret: KeyData() + ), + MockLock( + id: UUID(uuidString: "2AF2BFF2-F826-4154-AA61-E2D41C45CF34")!, + status: .unlock, + sharedSecret: KeyData() + ) + ] +} diff --git a/Xcode/LockKit/Model/Mock/MockScanData.swift b/Xcode/LockKit/Model/Mock/MockScanData.swift new file mode 100644 index 00000000..a9a718b9 --- /dev/null +++ b/Xcode/LockKit/Model/Mock/MockScanData.swift @@ -0,0 +1,79 @@ +// +// MockScanData.swift +// BluetoothExplorer +// +// Created by Alsey Coleman Miller on 31/10/21. +// Copyright © 2021 Alsey Coleman Miller. All rights reserved. +// + +#if DEBUG +import Foundation +import Bluetooth +import GATT + +public typealias MockScanData = ScanData + +public extension MockScanData { + + static let beacon = MockScanData( + peripheral: .beacon, + date: Date(timeIntervalSinceReferenceDate: 10_000), + rssi: -20, + advertisementData: .beacon, + isConnectable: true + ) + + static let smartThermostat = MockScanData( + peripheral: .smartThermostat, + date: Date(timeIntervalSinceReferenceDate: 10_100), + rssi: -127, + advertisementData: .smartThermostat, + isConnectable: true + ) + + static func lock(_ id: UInt8) -> MockScanData { + MockScanData( + peripheral: .lock(id), + date: Date(timeIntervalSinceReferenceDate: 10_100), + rssi: -127, + advertisementData: .lock, + isConnectable: true + ) + } + + static var lock: MockScanData { + .lock(0x01) + } +} + +public extension MockCentral.Peripheral { + + static var random: MockCentral.Peripheral { + Peripheral(id: BluetoothAddress(bytes: (.random(in: .min ... .max), .random(in: .min ... .max), .random(in: .min ... .max), .random(in: .min ... .max), .random(in: .min ... .max), .random(in: .min ... .max)))) + } + + static var beacon: Peripheral { + Peripheral(id: BluetoothAddress(rawValue: "00:AA:AB:03:10:01")!) + } + + static var smartThermostat: Peripheral { + Peripheral(id: BluetoothAddress(rawValue: "00:1A:7D:DA:71:13")!) + } + + static func lock(_ id: UInt8) -> Peripheral { + Peripheral(id: BluetoothAddress(bytes: (0x00, 0xAA, 0xBB, 0xCC, 0xDD, id))) + } + + static var lock: Peripheral { + .lock(0x01) + } +} + +public extension MockCentral.Peripheral.ID { + + static var random: MockCentral.Peripheral.ID { + return MockCentral.Peripheral.random.id + } +} + +#endif diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 34de1d14..c1726e23 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -35,9 +35,9 @@ public final class Store: ObservableObject { @Published public var lockInformation = [NativePeripheral: LockInformation]() - public lazy var central = DarwinCentral() + public lazy var central = NativeCentral.shared - private var scanStream: AsyncCentralScan? + private var scanStream: AsyncCentralScan? public lazy var preferences = Preferences(suiteName: .lock)! @@ -454,7 +454,7 @@ public extension Store { } @discardableResult - func readInformation(for peripheral: DarwinCentral.Peripheral) async throws -> LockInformation { + func readInformation(for peripheral: NativePeripheral) async throws -> LockInformation { guard await central.state == .poweredOn else { throw LockError.bluetoothUnavailable } @@ -478,7 +478,7 @@ public extension Store { /// Setup a lock. func setup( - for lock: DarwinCentral.Peripheral, + for lock: NativePeripheral, using sharedSecret: KeyData, name: String ) async throws { @@ -526,7 +526,7 @@ public extension Store { } func newKey( - for peripheral: DarwinCentral.Peripheral, + for peripheral: NativePeripheral, permission: Permission, name newKeyName: String ) async throws -> NewKey.Invitation { @@ -750,7 +750,7 @@ public extension Store { @discardableResult func listEvents( - for peripheral: NativeCentral.Peripheral, + for peripheral: NativePeripheral, fetchRequest: LockEvent.FetchRequest? = nil ) async throws -> [LockEvent] { stopScanning() diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 4fdd444f..c043c64c 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -97,6 +97,11 @@ 6E84E54128DE8728008CAE85 /* UnlockActionAppEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E54028DE8728008CAE85 /* UnlockActionAppEnum.swift */; }; 6E84E54328DE878D008CAE85 /* PermissionAppEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E54228DE878D008CAE85 /* PermissionAppEnum.swift */; }; 6E84E54628DEA0CE008CAE85 /* FetchEventsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E54428DE89BC008CAE85 /* FetchEventsIntent.swift */; }; + 6E84E54C28DF4A98008CAE85 /* MockAdvertisement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E54828DF4A98008CAE85 /* MockAdvertisement.swift */; }; + 6E84E54D28DF4A98008CAE85 /* MockCentral.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E54928DF4A98008CAE85 /* MockCentral.swift */; }; + 6E84E54E28DF4A98008CAE85 /* MockAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E54A28DF4A98008CAE85 /* MockAttributes.swift */; }; + 6E84E54F28DF4A98008CAE85 /* MockScanData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E54B28DF4A98008CAE85 /* MockScanData.swift */; }; + 6E84E55128DF59D1008CAE85 /* MockLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E55028DF59D1008CAE85 /* MockLock.swift */; }; 6E8BBFDA28DC2CA000F03735 /* SetupLockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */; }; 6E8BBFDC28DCC8F300F03735 /* AsyncFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */; }; 6E8BBFEB28DD301B00F03735 /* LockIntentsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFEA28DD301B00F03735 /* LockIntentsExtension.swift */; }; @@ -230,6 +235,11 @@ 6E84E54028DE8728008CAE85 /* UnlockActionAppEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockActionAppEnum.swift; sourceTree = ""; }; 6E84E54228DE878D008CAE85 /* PermissionAppEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionAppEnum.swift; sourceTree = ""; }; 6E84E54428DE89BC008CAE85 /* FetchEventsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchEventsIntent.swift; sourceTree = ""; }; + 6E84E54828DF4A98008CAE85 /* MockAdvertisement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockAdvertisement.swift; sourceTree = ""; }; + 6E84E54928DF4A98008CAE85 /* MockCentral.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockCentral.swift; sourceTree = ""; }; + 6E84E54A28DF4A98008CAE85 /* MockAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockAttributes.swift; sourceTree = ""; }; + 6E84E54B28DF4A98008CAE85 /* MockScanData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockScanData.swift; sourceTree = ""; }; + 6E84E55028DF59D1008CAE85 /* MockLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockLock.swift; sourceTree = ""; }; 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupLockView.swift; sourceTree = ""; }; 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncFetchRequest.swift; sourceTree = ""; }; 6E8BBFE828DD301B00F03735 /* LockIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.extensionkit-extension"; includeInIndex = 0; path = LockIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -387,6 +397,7 @@ 6E3276DA28D7136400AF171B /* Model */ = { isa = PBXGroup; children = ( + 6E84E54728DF4A2C008CAE85 /* Mock */, 6E21835128D9516A00A622B3 /* iCloud */, 6E21832428D8F3B500A622B3 /* CoreData */, 6E21830D28D7FF2400A622B3 /* AppGroup.swift */, @@ -492,6 +503,18 @@ path = AppEnum; sourceTree = ""; }; + 6E84E54728DF4A2C008CAE85 /* Mock */ = { + isa = PBXGroup; + children = ( + 6E84E54828DF4A98008CAE85 /* MockAdvertisement.swift */, + 6E84E54A28DF4A98008CAE85 /* MockAttributes.swift */, + 6E84E54928DF4A98008CAE85 /* MockCentral.swift */, + 6E84E54B28DF4A98008CAE85 /* MockScanData.swift */, + 6E84E55028DF59D1008CAE85 /* MockLock.swift */, + ); + path = Mock; + sourceTree = ""; + }; 6E8BBFE928DD301B00F03735 /* LockIntents */ = { isa = PBXGroup; children = ( @@ -814,6 +837,7 @@ 6E21833B28D8F3B500A622B3 /* ConfirmNewKeyEventManagedObject.swift in Sources */, 6E21830C28D7FEF600A622B3 /* FileManager.swift in Sources */, 6E9E37AB28DAA6D100BE7128 /* KeyDetailView.swift in Sources */, + 6E84E54C28DF4A98008CAE85 /* MockAdvertisement.swift in Sources */, 6E6A97EF28DBEC2D00C689F6 /* NewKeyInvitationStore.swift in Sources */, 6E4CB61028D7867700116573 /* LockRowView.swift in Sources */, 6E21835028D9506100A622B3 /* Preferences.swift in Sources */, @@ -824,6 +848,7 @@ 6E21833C28D8F3B500A622B3 /* KeyManagedObject.swift in Sources */, 6E21830128D7C37500A622B3 /* Error.swift in Sources */, 6E84E53428DDDCDC008CAE85 /* Bundle.swift in Sources */, + 6E84E54E28DF4A98008CAE85 /* MockAttributes.swift in Sources */, 6E21836328D9516B00A622B3 /* iCloud.swift in Sources */, 6E21833E28D8F3B500A622B3 /* CreateNewKeyEventManagedObject.swift in Sources */, 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */, @@ -844,9 +869,11 @@ 6E8BBFDA28DC2CA000F03735 /* SetupLockView.swift in Sources */, 6E9E37AD28DADF2600BE7128 /* ActivityView.swift in Sources */, 6E21834328D8F3B500A622B3 /* ManagedObject.swift in Sources */, + 6E84E54D28DF4A98008CAE85 /* MockCentral.swift in Sources */, 6E84E53628DDE01F008CAE85 /* AssetExtractor.swift in Sources */, 6E21834028D8F3B500A622B3 /* RemoveKeyEventManagedObject.swift in Sources */, 6E21834A28D90F7A00A622B3 /* LocalAuthentication.swift in Sources */, + 6E84E54F28DF4A98008CAE85 /* MockScanData.swift in Sources */, 6E21833628D8F3B500A622B3 /* ContactManagedObject.swift in Sources */, 6E3276D228D70CE100AF171B /* Store.swift in Sources */, 6E4CB61A28D788AA00116573 /* UIStyleKit.swift in Sources */, @@ -856,6 +883,7 @@ 6E21836128D9516B00A622B3 /* CloudKit.swift in Sources */, 6E21836428D9516B00A622B3 /* CloudUser.swift in Sources */, 6E21834E28D91FDC00A622B3 /* Event.swift in Sources */, + 6E84E55128DF59D1008CAE85 /* MockLock.swift in Sources */, 6E21830E28D7FF2400A622B3 /* AppGroup.swift in Sources */, 6E21836728D9516B00A622B3 /* CloudNewKey.swift in Sources */, 6E6A97F328DC030000C689F6 /* NewKeyInvitationView.swift in Sources */, diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index 8e2b0702..73bbf0e0 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -251,13 +251,13 @@ struct NearbyDevicesView_Previews: PreviewProvider { NearbyDevicesView.StateView( state: .scanning, items: [ - .loading(UUID()), - .setup(UUID(), UUID()), - .unknown(UUID(), UUID()), - .key(UUID(), "My lock", .admin) + .loading(.random), + .setup(.random, UUID()), + .unknown(.random, UUID()), + .key(.random, "My lock", .admin) ], toggleScan: { }, - destination: { Text(verbatim: $0.id.uuidString) } + destination: { Text(verbatim: $0.id.description) } ) } } From 57a000d9d7479bb3ea3fe21016b0d392430fc685 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 24 Sep 2022 10:13:36 -0700 Subject: [PATCH 147/229] [App] Updated mocked devices --- Xcode/LockIntents/AppEntity/KeyEntity.swift | 2 +- Xcode/LockIntents/AppEntity/LockEntity.swift | 4 +- .../AppEnum/LockStatusAppEnum.swift | 35 ++++--- .../AppEnum/PermissionAppEnum.swift | 66 ++++++------- .../AppEnum/UnlockActionAppEnum.swift | 37 ++++---- .../AppIntent/FetchEventsIntent.swift | 6 -- .../AppIntent/ScanLocksIntent.swift | 12 ++- .../LockIntents/AppIntent/UnlockIntent.swift | 3 +- Xcode/LockKit/Model/Mock/MockCentral.swift | 83 +++++++---------- Xcode/LockKit/Model/Mock/MockLock.swift | 92 ++++++++++++++++++- 10 files changed, 201 insertions(+), 139 deletions(-) diff --git a/Xcode/LockIntents/AppEntity/KeyEntity.swift b/Xcode/LockIntents/AppEntity/KeyEntity.swift index 2ba13f26..cfd7b3ec 100644 --- a/Xcode/LockIntents/AppEntity/KeyEntity.swift +++ b/Xcode/LockIntents/AppEntity/KeyEntity.swift @@ -24,7 +24,7 @@ struct KeyEntity: AppEntity, Identifiable { var created: Date /// Key's permissions. - var permission: Permission + var permission: PermissionAppEnum } @available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) diff --git a/Xcode/LockIntents/AppEntity/LockEntity.swift b/Xcode/LockIntents/AppEntity/LockEntity.swift index 568a5f43..afaf991b 100644 --- a/Xcode/LockIntents/AppEntity/LockEntity.swift +++ b/Xcode/LockIntents/AppEntity/LockEntity.swift @@ -21,10 +21,10 @@ struct LockEntity: AppEntity, Identifiable { var version: String /// Device state - var status: LockStatus + var status: LockStatusAppEnum /// Supported lock actions - var unlockActions: Set + var unlockActions: Set /// Stored name var name: String? diff --git a/Xcode/LockIntents/AppEnum/LockStatusAppEnum.swift b/Xcode/LockIntents/AppEnum/LockStatusAppEnum.swift index a38afedf..bb123112 100644 --- a/Xcode/LockIntents/AppEnum/LockStatusAppEnum.swift +++ b/Xcode/LockIntents/AppEnum/LockStatusAppEnum.swift @@ -9,25 +9,22 @@ import Foundation import AppIntents @available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) -extension LockEntity { +enum LockStatusAppEnum: UInt8, AppEnum { - enum LockStatus: UInt8, AppEnum { - - /// Initial Status - case setup = 0x00 - - /// Idle / Unlock Mode - case unlock = 0x01 - - static var typeDisplayRepresentation: TypeDisplayRepresentation { - "Lock Status" - } - - static var caseDisplayRepresentations: [LockStatus : DisplayRepresentation] { - [ - .setup: "Needs Setup", - .unlock: "Ready to Unlock" - ] - } + /// Initial Status + case setup = 0x00 + + /// Idle / Unlock Mode + case unlock = 0x01 + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Lock Status" + } + + static var caseDisplayRepresentations: [LockStatusAppEnum : DisplayRepresentation] { + [ + .setup: "Needs Setup", + .unlock: "Ready to Unlock" + ] } } diff --git a/Xcode/LockIntents/AppEnum/PermissionAppEnum.swift b/Xcode/LockIntents/AppEnum/PermissionAppEnum.swift index 9719e6a1..44af6457 100644 --- a/Xcode/LockIntents/AppEnum/PermissionAppEnum.swift +++ b/Xcode/LockIntents/AppEnum/PermissionAppEnum.swift @@ -9,40 +9,40 @@ import Foundation import AppIntents @available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) -extension KeyEntity { +enum PermissionAppEnum: UInt8, AppEnum { - enum Permission: UInt8, AppEnum { - - case owner = 0x00 - case admin = 0x01 - case anytime = 0x02 - case scheduled = 0x03 - - static var typeDisplayRepresentation: TypeDisplayRepresentation { - "Permission" - } - - static var caseDisplayRepresentations: [Permission : DisplayRepresentation] { - [ - .owner: "Owner", - .admin: "Admin", - .anytime: "Anytime", - .scheduled: "Scheduled" - ] - } - - var imageName: String { - switch self { - case .owner: - return "permissionOwner" - case .admin: - return "permissionAdmin" - case .anytime: - return "permissionAnytime" - case .scheduled: - return "permissionScheduled" - } - } + case owner = 0x00 + case admin = 0x01 + case anytime = 0x02 + case scheduled = 0x03 + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Permission" + } + + static var caseDisplayRepresentations: [PermissionAppEnum : DisplayRepresentation] { + [ + .owner: "Owner", + .admin: "Admin", + .anytime: "Anytime", + .scheduled: "Scheduled" + ] } } +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +extension PermissionAppEnum { + + var imageName: String { + switch self { + case .owner: + return "permissionOwner" + case .admin: + return "permissionAdmin" + case .anytime: + return "permissionAnytime" + case .scheduled: + return "permissionScheduled" + } + } +} diff --git a/Xcode/LockIntents/AppEnum/UnlockActionAppEnum.swift b/Xcode/LockIntents/AppEnum/UnlockActionAppEnum.swift index 90db09ac..ee350a57 100644 --- a/Xcode/LockIntents/AppEnum/UnlockActionAppEnum.swift +++ b/Xcode/LockIntents/AppEnum/UnlockActionAppEnum.swift @@ -9,25 +9,22 @@ import Foundation import AppIntents @available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) -extension LockEntity { - - enum UnlockAction: UInt8, AppEnum { - - /// Unlock immediately. - case `default` = 0b01 - - /// Unlock when button is pressed. - case button = 0b10 - - static var typeDisplayRepresentation: TypeDisplayRepresentation { - "Unlock Action" - } - - static var caseDisplayRepresentations: [UnlockAction : DisplayRepresentation] { - [ - .default: "Default", - .button: "Button" - ] - } +enum UnlockActionAppEnum: UInt8, AppEnum { + + /// Unlock immediately. + case `default` = 0b01 + + /// Unlock when button is pressed. + case button = 0b10 + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Unlock Action" + } + + static var caseDisplayRepresentations: [UnlockActionAppEnum : DisplayRepresentation] { + [ + .default: "Default", + .button: "Button" + ] } } diff --git a/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift b/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift index e473e412..bef91fc2 100644 --- a/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift +++ b/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift @@ -97,9 +97,3 @@ struct FetchEventsIntent: AppIntent { ) } } - -@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) -extension FetchEventsIntent { - - -} diff --git a/Xcode/LockIntents/AppIntent/ScanLocksIntent.swift b/Xcode/LockIntents/AppIntent/ScanLocksIntent.swift index 01084ba7..a1620b44 100644 --- a/Xcode/LockIntents/AppIntent/ScanLocksIntent.swift +++ b/Xcode/LockIntents/AppIntent/ScanLocksIntent.swift @@ -65,10 +65,16 @@ private extension ScanLocksIntent { VStack(alignment: .leading, spacing: 8) { if results.isEmpty { Text("No locks found.") + .padding(20) } else { - ForEach(results) { - view(for: $0) - .padding(8) + if results.count > 3 { + Text("Found \(results.count) locks.") + .padding(20) + } else { + ForEach(results) { + view(for: $0) + .padding(8) + } } } } diff --git a/Xcode/LockIntents/AppIntent/UnlockIntent.swift b/Xcode/LockIntents/AppIntent/UnlockIntent.swift index 02c192de..5a4aa632 100644 --- a/Xcode/LockIntents/AppIntent/UnlockIntent.swift +++ b/Xcode/LockIntents/AppIntent/UnlockIntent.swift @@ -8,7 +8,7 @@ import AppIntents import SwiftUI import LockKit -/* + @available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) struct UnlockIntent: AppIntent { @@ -38,4 +38,3 @@ struct UnlockIntent: AppIntent { return .result() } } -*/ diff --git a/Xcode/LockKit/Model/Mock/MockCentral.swift b/Xcode/LockKit/Model/Mock/MockCentral.swift index 72e7bc1b..86f97262 100644 --- a/Xcode/LockKit/Model/Mock/MockCentral.swift +++ b/Xcode/LockKit/Model/Mock/MockCentral.swift @@ -70,7 +70,17 @@ public final class MockCentral: CentralManager { guard state == .poweredOn else { throw DarwinCentralError.invalidState(state) } - try await Task.sleep(timeInterval: 1.0) + await self.storage.updateState { + $0.isScanning = true + } + defer { + Task { + await self.storage.updateState { + $0.isScanning = false + } + } + } + try await Task.sleep(timeInterval: 0.2) for scanData in await self.storage.state.scanData { // apply filter if services.isEmpty == false { @@ -79,7 +89,10 @@ public final class MockCentral: CentralManager { continue } } - try await Task.sleep(timeInterval: 0.3) + try await Task.sleep(timeInterval: 0.1) + guard await self.storage.state.isScanning else { + continue + } continuation(scanData) } } @@ -315,64 +328,30 @@ internal extension MockCentral { struct State { var isScanning = false - var scanData: [MockScanData] = [.beacon, .smartThermostat, .lock, .lock(2), .lock(3)] + var scanData: [MockScanData] = [.beacon, .smartThermostat] + MockLock.locks.enumerated().map { .lock(UInt8($0.offset)) } var connected = Set() - var characteristics: [MockService: [MockCharacteristic]] = [ - .deviceInformation: [ - .deviceName, - .manufacturerName, - .modelNumber, - .serialNumber - ], - .battery: [ - .batteryLevel - ], - .savantSystems: [ - .savantTest - ], - .lock(0x01): [ - .lockInformation(0x01) - ], - .lock(0x02): [ - .lockInformation(0x02) - ], - .lock(0x03): [ - .lockInformation(0x03) - ] - ] + var characteristics: [MockService: [MockCharacteristic]] = { + let characteristics = MockLock.locks.enumerated().map { (index, lock) in + let id = UInt8(index) + return (MockService.lock(id), [ + Characteristic.lockInformation(id) + ]) + } + return .init(uniqueKeysWithValues: characteristics) + }() var descriptors: [MockCharacteristic: [MockDescriptor]] = [ .batteryLevel: [.clientCharacteristicConfiguration(.beacon)], .savantTest: [.clientCharacteristicConfiguration(.smartThermostat)], ] - var characteristicValues: [MockCharacteristic: Data] = [ - .deviceName: Data("iBeacon".utf8), - .manufacturerName: Data("Apple Inc.".utf8), - .modelNumber: Data("iPhone11.8".utf8), - .serialNumber: Data(UUID().uuidString.utf8), - .batteryLevel: Data([100]), - .savantTest: Data(UUID().uuidString.utf8), - .lockInformation(0x01): LockInformationCharacteristic( - id: UUID(), - buildVersion: .current, - version: .current, - status: .unlock, - unlockActions: [.default] - ).data, - .lockInformation(0x02): LockInformationCharacteristic( - id: UUID(), + var characteristicValues: [MockCharacteristic: Data] = .init(uniqueKeysWithValues: MockLock.locks.enumerated().map({ (index, lock) in + (.lockInformation(UInt8(index)), LockInformationCharacteristic( + id: lock.id, buildVersion: .current, version: .current, - status: .unlock, + status: lock.status, unlockActions: [.default] - ).data, - .lockInformation(0x03): LockInformationCharacteristic( - id: UUID(), - buildVersion: .current, - version: .current, - status: .unlock, - unlockActions: [.default] - ).data - ] + ).data) + })) var descriptorValues: [MockDescriptor: Data] = [ .clientCharacteristicConfiguration(.beacon): Data([0x00]), .clientCharacteristicConfiguration(.smartThermostat): Data([0x00]), diff --git a/Xcode/LockKit/Model/Mock/MockLock.swift b/Xcode/LockKit/Model/Mock/MockLock.swift index 97a9d426..f81621c6 100644 --- a/Xcode/LockKit/Model/Mock/MockLock.swift +++ b/Xcode/LockKit/Model/Mock/MockLock.swift @@ -34,6 +34,96 @@ public extension MockLock { id: UUID(uuidString: "2AF2BFF2-F826-4154-AA61-E2D41C45CF34")!, status: .unlock, sharedSecret: KeyData() - ) + ), + MockLock( + id: UUID(), + status: .unlock, + sharedSecret: KeyData() + ), + MockLock( + id: UUID(), + status: .unlock, + sharedSecret: KeyData() + ), + MockLock( + id: UUID(), + status: .unlock, + sharedSecret: KeyData() + ), + MockLock( + id: UUID(), + status: .unlock, + sharedSecret: KeyData() + ), + MockLock( + id: UUID(), + status: .unlock, + sharedSecret: KeyData() + ), + MockLock( + id: UUID(), + status: .unlock, + sharedSecret: KeyData() + ), + MockLock( + id: UUID(), + status: .unlock, + sharedSecret: KeyData() + ), + MockLock( + id: UUID(), + status: .unlock, + sharedSecret: KeyData() + ), + MockLock( + id: UUID(), + status: .unlock, + sharedSecret: KeyData() + ), + MockLock( + id: UUID(), + status: .unlock, + sharedSecret: KeyData() + ), + MockLock( + id: UUID(), + status: .unlock, + sharedSecret: KeyData() + ), + MockLock( + id: UUID(), + status: .unlock, + sharedSecret: KeyData() + ), + MockLock( + id: UUID(), + status: .unlock, + sharedSecret: KeyData() + ), + MockLock( + id: UUID(), + status: .unlock, + sharedSecret: KeyData() + ), + MockLock( + id: UUID(), + status: .unlock, + sharedSecret: KeyData() + ), + MockLock( + id: UUID(), + status: .unlock, + sharedSecret: KeyData() + ), + MockLock( + id: UUID(), + status: .unlock, + sharedSecret: KeyData() + ), + MockLock( + id: UUID(), + status: .unlock, + sharedSecret: KeyData() + ), ] } From c1d6c13e2cb78d0c2cccc8d8360ad5c3091a2b53 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 24 Sep 2022 14:25:08 -0700 Subject: [PATCH 148/229] [App] Updated mock data --- Xcode/LockKit/Model/Mock/MockCentral.swift | 2 +- Xcode/LockKit/Model/Mock/MockLock.swift | 37 +++++++++++++++++++++- Xcode/LockKit/Model/Store.swift | 3 ++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/Xcode/LockKit/Model/Mock/MockCentral.swift b/Xcode/LockKit/Model/Mock/MockCentral.swift index 86f97262..adf2233d 100644 --- a/Xcode/LockKit/Model/Mock/MockCentral.swift +++ b/Xcode/LockKit/Model/Mock/MockCentral.swift @@ -46,7 +46,7 @@ public final class MockCentral: CentralManager { internal init() { Task { - try await Task.sleep(timeInterval: 0.5) + try await Task.sleep(timeInterval: 0.3) await self.storage.stateDidChange(.poweredOn) } } diff --git a/Xcode/LockKit/Model/Mock/MockLock.swift b/Xcode/LockKit/Model/Mock/MockLock.swift index f81621c6..a4d3a0cc 100644 --- a/Xcode/LockKit/Model/Mock/MockLock.swift +++ b/Xcode/LockKit/Model/Mock/MockLock.swift @@ -8,6 +8,7 @@ import Foundation import CoreLock +#if DEBUG public struct MockLock: Equatable, Hashable, Codable, Identifiable { public let id: UUID @@ -15,6 +16,39 @@ public struct MockLock: Equatable, Hashable, Codable, Identifiable { public var status: LockStatus public var sharedSecret: KeyData + + public var keys: [Key] = [] +} + +internal extension Store { + + func insertMockData() { + let maxLocks = 3 + for index in 0 ..< maxLocks { + let lock = MockLock.locks[index] + let key = self.applicationData.locks[lock.id]?.key ?? Key( + id: UUID(), + name: "Owner", + created: Date() - TimeInterval(60 * (maxLocks - index + 1)), + permission: .owner + ) + // add lock and key to file + self.applicationData.locks[lock.id] = .init( + key: key, + name: "My lock \(index + 1)", + information: .init( + buildVersion: .current, + version: .current, + status: .unlock, + unlockActions: [.default] + ) + ) + // insert key data into keychain + if self[key: key.id] == nil { + self[key: key.id] = KeyData() + } + } + } } public extension MockLock { @@ -22,7 +56,7 @@ public extension MockLock { static var locks: [MockLock] = [ MockLock( id: UUID(uuidString: "669A06D7-5AE5-431B-971C-7A118E77CA51")!, - status: .setup, + status: .unlock, sharedSecret: KeyData() ), MockLock( @@ -127,3 +161,4 @@ public extension MockLock { ), ] } +#endif diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index c1726e23..ee722813 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -74,6 +74,9 @@ public final class Store: ObservableObject { Task { await lockCacheChanged() } + #if targetEnvironment(simulator) + insertMockData() + #endif } } From 8513e6dd548e8feb087e0377bb7795712bac3eef Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 24 Sep 2022 14:25:45 -0700 Subject: [PATCH 149/229] [App] Updated `NearbyDevicesView` sorting --- Xcode/SmartLock/View/NearbyDevicesView.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index 73bbf0e0..900a9b9a 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -67,10 +67,12 @@ private extension NearbyDevicesView { } var peripherals: [NativePeripheral] { - store.peripherals + store.peripherals.keys .lazy - .sorted(by: { $0.value.rssi < $1.value.rssi }) - .map { $0.key } + .sorted(by: { store.lockInformation[$0]?.id.description ?? "" > store.lockInformation[$1]?.id.description ?? "" }) + .sorted(by: { + store.applicationData.locks[store.lockInformation[$0]?.id ?? UUID()]?.key.created ?? .distantFuture > store.applicationData.locks[store.lockInformation[$1]?.id ?? UUID()]?.key.created ?? .distantFuture }) + .sorted(by: { $0.description < $1.description }) } var state: State { From 0044f9de52dbdb1ee26c86ca4d93caad6980f1a8 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 24 Sep 2022 16:46:56 -0700 Subject: [PATCH 150/229] [App] Added `View.alert(error:)` --- Xcode/LockKit/View/ErrorView.swift | 57 ++++++++++++++++++++ Xcode/LockKit/View/LockDetailView.swift | 7 +++ Xcode/SmartLock.xcodeproj/project.pbxproj | 4 ++ Xcode/SmartLock/View/NearbyDevicesView.swift | 2 + 4 files changed, 70 insertions(+) create mode 100644 Xcode/LockKit/View/ErrorView.swift diff --git a/Xcode/LockKit/View/ErrorView.swift b/Xcode/LockKit/View/ErrorView.swift new file mode 100644 index 00000000..626e32fe --- /dev/null +++ b/Xcode/LockKit/View/ErrorView.swift @@ -0,0 +1,57 @@ +// +// ErrorView.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/24/22. +// + +import SwiftUI + +public extension View { + + func alert(error: Binding) -> some View { + return alert( + isPresented: Binding( + get: { error.wrappedValue != nil }, + set: { + if $0 == false { + error.wrappedValue = nil + } + } + ), + content: { + Alert( + title: Text("Error"), + message: Text(verbatim: error.wrappedValue?.localizedDescription ?? "Unknown error"), + dismissButton: .cancel(Text("Ok"), action: { + error.wrappedValue = nil + }) + ) + } + ) + } +} + +#if DEBUG +struct ErrorView_Previews: PreviewProvider { + + static var previews: some View { + NavigationView { + PreviewView() + } + } + + struct PreviewView: View { + + @State + private var error: Error? + + var body: some View { + Button("Show Error") { + error = LockError.notInRange(lock: UUID()) + } + .alert(error: $error) + } + } +} +#endif diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 909486af..d6e94ef7 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -28,6 +28,9 @@ public struct LockDetailView: View { @State private var showNewKeyModal = false + @State + private var error: Error? + public init(id: UUID) { self.id = id } @@ -49,6 +52,7 @@ public struct LockDetailView: View { .onAppear { reload() } + .alert(error: $error) .newPermissionSheet( for: id, isPresented: $showNewKeyModal, @@ -213,6 +217,8 @@ private extension LockDetailView { } catch { log("⚠️ Unable to authenticate for unlock \(id). \(error)") + self.error = error + return } // cancel all operation if store.isScanning { @@ -226,6 +232,7 @@ private extension LockDetailView { try await store.unlock(for: id, action: .default) } catch { log("⚠️ Unable to unlock \(id). \(error)") + self.error = error } } } diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index c043c64c..35fd5811 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 6E10C90028DFC6C400703691 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C8FF28DFC6C400703691 /* ErrorView.swift */; }; 6E169A7828D9A34F008545EC /* NavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7728D9A34F008545EC /* NavigationLink.swift */; }; 6E169A7D28D9C097008545EC /* PermissionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7C28D9C097008545EC /* PermissionsView.swift */; }; 6E169A7F28D9C135008545EC /* NewPermissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7E28D9C135008545EC /* NewPermissionView.swift */; }; @@ -151,6 +152,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 6E10C8FF28DFC6C400703691 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; 6E169A7728D9A34F008545EC /* NavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationLink.swift; sourceTree = ""; }; 6E169A7C28D9C097008545EC /* PermissionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsView.swift; sourceTree = ""; }; 6E169A7E28D9C135008545EC /* NewPermissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPermissionView.swift; sourceTree = ""; }; @@ -437,6 +439,7 @@ 6E9E37AA28DAA6D100BE7128 /* KeyDetailView.swift */, 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */, 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */, + 6E10C8FF28DFC6C400703691 /* ErrorView.swift */, ); path = View; sourceTree = ""; @@ -866,6 +869,7 @@ 6E21836528D9516B00A622B3 /* CloudLock.swift in Sources */, 6E21834228D8F3B500A622B3 /* UnlockEventManagedObject.swift in Sources */, 6E21833528D8F3B500A622B3 /* SetupEventManagedObject.swift in Sources */, + 6E10C90028DFC6C400703691 /* ErrorView.swift in Sources */, 6E8BBFDA28DC2CA000F03735 /* SetupLockView.swift in Sources */, 6E9E37AD28DADF2600BE7128 /* ActivityView.swift in Sources */, 6E21834328D8F3B500A622B3 /* ManagedObject.swift in Sources */, diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index 900a9b9a..9270d17c 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -247,6 +247,7 @@ extension LockRowView { // MARK: - Preview +#if targetEnvironment(simulator) struct NearbyDevicesView_Previews: PreviewProvider { static var previews: some View { NavigationView { @@ -264,3 +265,4 @@ struct NearbyDevicesView_Previews: PreviewProvider { } } } +#endif From f3b09923d4ba7e5d3ed463f1a2aff8a8e094fbfb Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 24 Sep 2022 16:47:16 -0700 Subject: [PATCH 151/229] [App] Added `LockError.errorDescription` --- Xcode/LockKit/Model/Error.swift | 46 +++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/Xcode/LockKit/Model/Error.swift b/Xcode/LockKit/Model/Error.swift index bfbd4697..6d8578a5 100644 --- a/Xcode/LockKit/Model/Error.swift +++ b/Xcode/LockKit/Model/Error.swift @@ -37,10 +37,35 @@ public enum LockError: Error { case newKeyExpired } -/* + // MARK: - CustomNSError -#if os(iOS) +extension LockError: LocalizedError { + + public var errorDescription: String? { + switch self { + case .unknownLock: + return "Unable to read lock information" + case let .notInRange(lock: lock): + return "Lock \(lock) Not in range" //R.string.error.notInRange() + case let .noKey(lock: lock): + return "No key for lock \(lock)" //R.string.error.noKey() + case let .notAdmin(lock: lock): + return "Not an admin of lock \(lock)" //R.string.error.notAdmin() + case .invalidQRCode: + return "Invalid QR code" //R.string.error.invalidQRCode() + case .invalidNewKeyFile: + return "Invalid key invitation" //R.string.error.invalidNewKeyFile() + case let .existingKey(lock: lock): + return "You already have an existing key for lock \(lock)" //R.string.error.existingKey() + case .newKeyExpired: + return "Key invitation expired" //R.string.error.newKeyExpired() + case .bluetoothUnavailable: + return "Bluetooth unavailable" + } + } +} + extension LockError: CustomNSError { public enum UserInfoKey: String { @@ -56,30 +81,29 @@ extension LockError: CustomNSError { public var errorUserInfo: [String : Any] { var userInfo = [String : Any](minimumCapacity: 2) + userInfo[NSLocalizedDescriptionKey] = self.errorDescription switch self { case let .notInRange(lock: lock): - userInfo[NSLocalizedDescriptionKey] = R.string.error.notInRange() userInfo[UserInfoKey.lock.rawValue] = lock as NSUUID 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() + break case .invalidNewKeyFile: - userInfo[NSLocalizedDescriptionKey] = R.string.error.invalidNewKeyFile() + break case let .existingKey(lock: lock): - userInfo[NSLocalizedDescriptionKey] = R.string.error.existingKey() userInfo[UserInfoKey.lock.rawValue] = lock as NSUUID case .newKeyExpired: - userInfo[NSLocalizedDescriptionKey] = R.string.error.newKeyExpired() + break + case .bluetoothUnavailable: + break + case .unknownLock: + break } return userInfo } } -#endif -*/ From e79ff533ccf2a7057c1f6634310d153b0a769672 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 24 Sep 2022 16:47:28 -0700 Subject: [PATCH 152/229] [App] Updated intents --- Xcode/LockIntents/AppIntent/FetchEventsIntent.swift | 11 ++++------- Xcode/LockIntents/AppIntent/ScanLocksIntent.swift | 5 +++++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift b/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift index bef91fc2..766954ab 100644 --- a/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift +++ b/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift @@ -23,10 +23,6 @@ struct FetchEventsIntent: AppIntent { ) } - static var parameterSummary: some ParameterSummary { - Summary("Fetch the events for \(\.$lock)") - } - /// The specified lock to unlock. @Parameter( title: "Lock", @@ -45,9 +41,10 @@ struct FetchEventsIntent: AppIntent { /// The fetch limit of the fetch request. @Parameter( title: "Limit", - description: "The fetch limit of the fetch request." + description: "The fetch limit of the fetch request.", + default: 10 ) - var limit: Int? + var limit: Int /// The keys used to filter the results. @Parameter( @@ -76,7 +73,7 @@ struct FetchEventsIntent: AppIntent { let store = Store.shared let fetchRequest = LockEvent.FetchRequest( offset: UInt8(offset), - limit: limit.flatMap(UInt8.init), + limit: UInt8(limit), predicate: .init( keys: keys.map { $0.id }, start: start, diff --git a/Xcode/LockIntents/AppIntent/ScanLocksIntent.swift b/Xcode/LockIntents/AppIntent/ScanLocksIntent.swift index a1620b44..5439c135 100644 --- a/Xcode/LockIntents/AppIntent/ScanLocksIntent.swift +++ b/Xcode/LockIntents/AppIntent/ScanLocksIntent.swift @@ -39,6 +39,11 @@ struct ScanLocksIntent: AppIntent { try await store.central.wait(for: .poweredOn) try await store.scan(duration: duration) let locks = store.lockInformation + .lazy + .sorted(by: { + store.applicationData.locks[$0.value.id]?.name ?? "" + > store.applicationData.locks[$1.value.id]?.name ?? "" + }) .sorted(by: { $0.key.id.description < $1.key.id.description }) .map { $0.value } let lockCache = store.applicationData.locks From 044e905f933c9b7781557331c4432ab386a3923a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 24 Sep 2022 18:40:32 -0700 Subject: [PATCH 153/229] [App] Added `EventManagedObject.displayRepresentation()` --- Xcode/LockKit/Model/Event.swift | 71 +++++++++++++++++++++++++++++ Xcode/LockKit/View/EventsView.swift | 64 ++------------------------ 2 files changed, 76 insertions(+), 59 deletions(-) diff --git a/Xcode/LockKit/Model/Event.swift b/Xcode/LockKit/Model/Event.swift index 24c8b6af..1d7ff457 100644 --- a/Xcode/LockKit/Model/Event.swift +++ b/Xcode/LockKit/Model/Event.swift @@ -7,6 +7,7 @@ // import Foundation +import CoreData import CoreLock public extension LockEvent.EventType { @@ -26,3 +27,73 @@ public extension LockEvent.EventType { } } } + +public extension EventManagedObject { + + func displayRepresentation( + displayLockName: Bool, + in context: NSManagedObjectContext + ) throws -> (title: String, subtitle: String, needsKeys: Set) { + var needsKeys = Set() + guard let lock = self.lock?.identifier else { + fatalError("Missing lock identifier") + } + let eventType = type(of: self).eventType + let action: String + var keyName: String + let key = try self.key(in: context) + if key == nil { + needsKeys.insert(lock) + } + switch self { + case is SetupEventManagedObject: + action = "Setup" //R.string.locksEventsViewController.eventsSetup() + keyName = key?.name ?? "" + case is UnlockEventManagedObject: + action = "Unlocked" //R.string.locksEventsViewController.eventsUnlocked() + keyName = key?.name ?? "" + case let event as CreateNewKeyEventManagedObject: + if let newKey = try event.confirmKeyEvent(in: context)?.key(in: context)?.name { + action = "Shared \(newKey)" //R.string.locksEventsViewController.eventsSharedNamed(newKey) + } else if let newKey = try event.newKey(in: context)?.name { + action = "Shared \(newKey)" //R.string.locksEventsViewController.eventsSharedNamed(newKey) + } else { + action = "Shared key" //R.string.locksEventsViewController.eventsShared() + needsKeys.insert(lock) + } + keyName = key?.name ?? "" + case let event as ConfirmNewKeyEventManagedObject: + if let key = key, + let permission = PermissionType(rawValue: numericCast(key.permission)) { + action = "Recieved \(permission.localizedText) from \(key.name ?? "")" //R.string.locksEventsViewController.eventsCreated(key.name ?? "", permission.localizedText) + if let parentKey = try! event.createKeyEvent(in: context)?.key(in: context) { + keyName = "Shared by \(parentKey.name ?? "")" //R.string.locksEventsViewController.eventsSharedBy(parentKey.name ?? "") + } else { + keyName = "" + needsKeys.insert(lock) + } + } else { + action = "Created key" //R.string.locksEventsViewController.eventsCreatedNamed() + keyName = "" + needsKeys.insert(lock) + } + case let event as RemoveKeyEventManagedObject: + if let removedKey = try event.removedKey(in: context)?.name { + action = "Removed key \(removedKey)" //R.string.locksEventsViewController.eventsRemovedNamed(removedKey) + } else { + action = "Removed key" //R.string.locksEventsViewController.eventsRemoved() + needsKeys.insert(lock) + } + keyName = key?.name ?? "" + default: + fatalError("Invalid event \(self)") + } + + let lockName = self.lock?.name ?? "" + if displayLockName, // if filtering for a single lock + lockName.isEmpty == false { + keyName = keyName.isEmpty ? lockName : lockName + " - " + keyName + } + return (action, keyName, needsKeys) + } +} diff --git a/Xcode/LockKit/View/EventsView.swift b/Xcode/LockKit/View/EventsView.swift index 80431a0c..9d545cdd 100644 --- a/Xcode/LockKit/View/EventsView.swift +++ b/Xcode/LockKit/View/EventsView.swift @@ -164,67 +164,13 @@ private extension EventsView { } func row(for managedObject: EventManagedObject) -> some View { - var needsKeys = Set() - guard let lock = managedObject.lock?.identifier else { - fatalError("Missing identifier") - } let context = self.managedObjectContext let eventType = type(of: managedObject).eventType - let action: String - var keyName: String - let key = try! managedObject.key(in: context) - if key == nil { - needsKeys.insert(lock) - } - switch managedObject { - case is SetupEventManagedObject: - action = "Setup" //R.string.locksEventsViewController.eventsSetup() - keyName = key?.name ?? "" - case is UnlockEventManagedObject: - action = "Unlocked" //R.string.locksEventsViewController.eventsUnlocked() - keyName = key?.name ?? "" - case let event as CreateNewKeyEventManagedObject: - if let newKey = try! event.confirmKeyEvent(in: context)?.key(in: context)?.name { - action = "Shared \(newKey)" //R.string.locksEventsViewController.eventsSharedNamed(newKey) - } else if let newKey = try! event.newKey(in: context)?.name { - action = "Shared \(newKey)" //R.string.locksEventsViewController.eventsSharedNamed(newKey) - } else { - action = "Shared key" //R.string.locksEventsViewController.eventsShared() - needsKeys.insert(lock) - } - keyName = key?.name ?? "" - case let event as ConfirmNewKeyEventManagedObject: - if let key = key, - let permission = PermissionType(rawValue: numericCast(key.permission)) { - action = "Recieved \(permission.localizedText) from \(key.name ?? "")" //R.string.locksEventsViewController.eventsCreated(key.name ?? "", permission.localizedText) - if let parentKey = try! event.createKeyEvent(in: context)?.key(in: context) { - keyName = "Shared by \(parentKey.name ?? "")" //R.string.locksEventsViewController.eventsSharedBy(parentKey.name ?? "") - } else { - keyName = "" - needsKeys.insert(lock) - } - } else { - action = "Created key" //R.string.locksEventsViewController.eventsCreatedNamed() - keyName = "" - needsKeys.insert(lock) - } - case let event as RemoveKeyEventManagedObject: - if let removedKey = try! event.removedKey(in: context)?.name { - action = "Removed key \(removedKey)" //R.string.locksEventsViewController.eventsRemovedNamed(removedKey) - } else { - action = "Removed key" //R.string.locksEventsViewController.eventsRemoved() - needsKeys.insert(lock) - } - keyName = key?.name ?? "" - default: - fatalError("Invalid event \(managedObject)") - } - - let lockName = managedObject.lock?.name ?? "" - if (self.predicate?.keys?.count ?? 0) == 1, // if filtering for a single lock - lockName.isEmpty == false { - keyName = keyName.isEmpty ? lockName : lockName + " - " + keyName - } + let displayLockName = (self.predicate?.keys?.count ?? 0) == 1 + let (action, keyName, _) = try! managedObject.displayRepresentation( + displayLockName: displayLockName, + in: managedObjectContext + ) return LockRowView( image: .emoji(eventType.symbol), From 8b98be023548a4b666326f0db00a3f164da6f3e5 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 24 Sep 2022 18:40:52 -0700 Subject: [PATCH 154/229] [App] Added `LockEventQuery` --- .../AppEntity/LockEventEntity.swift | 97 +++++++++++++++++++ .../AppEnum/EventTypeAppEnum.swift | 52 ++++++++++ .../AppIntent/FetchEventsIntent.swift | 3 +- .../EntityQuery/LockEventQuery.swift | 39 ++++++++ .../ConfirmNewKeyEventManagedObject.swift | 2 +- .../CreateNewKeyEventManagedObject.swift | 2 +- .../Model/CoreData/EventManagedObject.swift | 13 ++- .../RemoveKeyEventManagedObject.swift | 2 +- .../CoreData/SetupEventManagedObject.swift | 2 +- .../CoreData/UnlockEventManagedObject.swift | 2 +- Xcode/LockKit/Model/Store.swift | 4 +- Xcode/SmartLock.xcodeproj/project.pbxproj | 12 +++ 12 files changed, 218 insertions(+), 12 deletions(-) create mode 100644 Xcode/LockIntents/AppEntity/LockEventEntity.swift create mode 100644 Xcode/LockIntents/AppEnum/EventTypeAppEnum.swift create mode 100644 Xcode/LockIntents/EntityQuery/LockEventQuery.swift diff --git a/Xcode/LockIntents/AppEntity/LockEventEntity.swift b/Xcode/LockIntents/AppEntity/LockEventEntity.swift new file mode 100644 index 00000000..ff00c62d --- /dev/null +++ b/Xcode/LockIntents/AppEntity/LockEventEntity.swift @@ -0,0 +1,97 @@ +// +// LockEventEntity.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/24/22. +// + +import Foundation +import CoreData +import AppIntents +import LockKit + +/// Lock Intent Entity +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +struct LockEventEntity: AppEntity, Identifiable { + + let id: UUID + + /// Date event was created + let date: Date + + /// Key that created this event. + let key: UUID + + /// Type of event + let type: EventTypeAppEnum +} + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +extension LockEventEntity { + + static var defaultQuery = LockEventQuery() + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Lock Event" + } + + @MainActor + var displayRepresentation: DisplayRepresentation { + do { + let context = Store.shared.managedObjectContext + guard let managedObject = try EventManagedObject.find(id, in: context) else { + return defaultDisplayRepresentation + } + let (title, subtitle, _) = try managedObject.displayRepresentation(displayLockName: true, in: context) + return DisplayRepresentation( + title: "\(title)", + subtitle: "\(subtitle)\n\(date.formatted(date: .numeric, time: .shortened))", + image: nil + ) + } catch { + assertionFailure("Unable to fetch event. \(error)") + return defaultDisplayRepresentation + } + } +} + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +private extension LockEventEntity { + + var defaultDisplayRepresentation: DisplayRepresentation { + DisplayRepresentation( + title: "\(String(type.symbol)) \(type.localizedStringResource)", // - \(lock.name ?? "Lock")", + subtitle: "\(date.formatted(date: .abbreviated, time: .shortened))", + image: nil + ) + } +} + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +extension LockEventEntity { + + init?(managedObject: EventManagedObject) { + guard let id = managedObject.identifier, + let date = managedObject.date, + let key = managedObject.key, + let eventType = EventTypeAppEnum(rawValue: Swift.type(of: managedObject).eventType.rawValue) + else { return nil } + + self.id = id + self.date = date + self.key = key + self.type = eventType + } +} + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +extension LockEventEntity { + + init(_ value: LockEvent) { + + self.id = value.id + self.date = value.date + self.key = value.key + self.type = .init(rawValue: value.type.rawValue)! + } +} diff --git a/Xcode/LockIntents/AppEnum/EventTypeAppEnum.swift b/Xcode/LockIntents/AppEnum/EventTypeAppEnum.swift new file mode 100644 index 00000000..22485c1d --- /dev/null +++ b/Xcode/LockIntents/AppEnum/EventTypeAppEnum.swift @@ -0,0 +1,52 @@ +// +// EventTypeAppEnum.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/24/22. +// + +import AppIntents +import LockKit + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +enum EventTypeAppEnum: String, AppEnum { + + case setup = "com.colemancda.Lock.Event.Setup" + case unlock = "com.colemancda.Lock.Event.Unlock" + case createNewKey = "com.colemancda.Lock.Event.CreateNewKey" + case confirmNewKey = "com.colemancda.Lock.Event.ConfirmNewKey" + case removeKey = "com.colemancda.Lock.Event.RemoveKey" + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Event" + } + + static var caseDisplayRepresentations: [EventTypeAppEnum : DisplayRepresentation] { + [ + .setup: "Setup", + .unlock: "Unlock", + .createNewKey: "Create New Key", + .confirmNewKey: "Confirm New Key", + .removeKey: "Remove Key" + ] + } +} + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +extension EventTypeAppEnum { + + var symbol: Character { + switch self { + case .setup: + return "🔐" + case .unlock: + return "🔓" + case .createNewKey: + return "🔏" + case .confirmNewKey: + return "🔑" + case .removeKey: + return "🗑" + } + } +} diff --git a/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift b/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift index 766954ab..5d689a8c 100644 --- a/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift +++ b/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift @@ -70,6 +70,7 @@ struct FetchEventsIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { + let limit = min(self.limit, Int(UInt8.max)) let store = Store.shared let fetchRequest = LockEvent.FetchRequest( offset: UInt8(offset), @@ -90,7 +91,7 @@ struct FetchEventsIntent: AppIntent { fetchRequest: fetchRequest ) return .result( - value: "\(events)" + value: events.map { LockEventEntity($0) } ) } } diff --git a/Xcode/LockIntents/EntityQuery/LockEventQuery.swift b/Xcode/LockIntents/EntityQuery/LockEventQuery.swift new file mode 100644 index 00000000..8f8157ad --- /dev/null +++ b/Xcode/LockIntents/EntityQuery/LockEventQuery.swift @@ -0,0 +1,39 @@ +// +// LockEventQuery.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/24/22. +// + +import Foundation +import CoreData +import AppIntents +import LockKit + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +struct LockEventQuery: EntityQuery { + + func entities(for identifiers: [UUID]) async throws -> [LockEventEntity] { + let context = await Store.shared.backgroundContext + return try await context.perform { + return try identifiers + .lazy + .compactMap { try EventManagedObject.find($0, in: context) } + .compactMap { .init(managedObject: $0) } + } + } + + func suggestedEntities() async throws -> [LockEventEntity] { + let eventsSuggested = 3 + let context = await Store.shared.backgroundContext + return try await context.perform { + let locks = try LockManagedObject.fetch(in: context) + var events = [EventManagedObject]() + events.reserveCapacity(locks.count * eventsSuggested) + for lock in locks { + events += try lock.lastEvents(count: eventsSuggested, in: context) + } + return events.compactMap { .init(managedObject: $0) } + } + } +} diff --git a/Xcode/LockKit/Model/CoreData/ConfirmNewKeyEventManagedObject.swift b/Xcode/LockKit/Model/CoreData/ConfirmNewKeyEventManagedObject.swift index 525a41dd..d1f5f9d9 100644 --- a/Xcode/LockKit/Model/CoreData/ConfirmNewKeyEventManagedObject.swift +++ b/Xcode/LockKit/Model/CoreData/ConfirmNewKeyEventManagedObject.swift @@ -13,7 +13,7 @@ import Predicate public final class ConfirmNewKeyEventManagedObject: EventManagedObject { - @nonobjc override class var eventType: LockEvent.EventType { return .confirmNewKey } + @nonobjc override public class var eventType: LockEvent.EventType { return .confirmNewKey } internal convenience init(_ value: LockEvent.ConfirmNewKey, lock: LockManagedObject, diff --git a/Xcode/LockKit/Model/CoreData/CreateNewKeyEventManagedObject.swift b/Xcode/LockKit/Model/CoreData/CreateNewKeyEventManagedObject.swift index 52fe730d..f968a47f 100644 --- a/Xcode/LockKit/Model/CoreData/CreateNewKeyEventManagedObject.swift +++ b/Xcode/LockKit/Model/CoreData/CreateNewKeyEventManagedObject.swift @@ -13,7 +13,7 @@ import Predicate public final class CreateNewKeyEventManagedObject: EventManagedObject { - @nonobjc override class var eventType: LockEvent.EventType { return .createNewKey } + @nonobjc override public class var eventType: LockEvent.EventType { return .createNewKey } internal convenience init(_ value: LockEvent.CreateNewKey, lock: LockManagedObject, context: NSManagedObjectContext) { diff --git a/Xcode/LockKit/Model/CoreData/EventManagedObject.swift b/Xcode/LockKit/Model/CoreData/EventManagedObject.swift index cf739d36..94fdecc0 100644 --- a/Xcode/LockKit/Model/CoreData/EventManagedObject.swift +++ b/Xcode/LockKit/Model/CoreData/EventManagedObject.swift @@ -13,7 +13,7 @@ import Predicate public class EventManagedObject: NSManagedObject { - @nonobjc class var eventType: LockEvent.EventType { fatalError("Implemented by subclass") } + @nonobjc public class var eventType: LockEvent.EventType { fatalError("Implemented by subclass") } internal static func initWith(_ value: LockEvent, lock: LockManagedObject, context: NSManagedObjectContext) -> EventManagedObject { @@ -31,7 +31,7 @@ public class EventManagedObject: NSManagedObject { } } - internal static func find(_ id: UUID, in context: NSManagedObjectContext) throws -> EventManagedObject? { + public static func find(_ id: UUID, in context: NSManagedObjectContext) throws -> EventManagedObject? { try context.find(identifier: id as NSUUID, propertyName: #keyPath(EventManagedObject.identifier), @@ -90,10 +90,15 @@ public extension EventManagedObject { public extension LockManagedObject { func lastEvent(in context: NSManagedObjectContext) throws -> EventManagedObject? { + return try lastEvents(count: 1, in: context).first + } + + func lastEvents(count: Int, in context: NSManagedObjectContext) throws -> [EventManagedObject] { let fetchRequest = NSFetchRequest() fetchRequest.entity = EventManagedObject.entity() - fetchRequest.fetchBatchSize = 10 + fetchRequest.fetchBatchSize = count + fetchRequest.fetchLimit = count fetchRequest.includesSubentities = true fetchRequest.shouldRefreshRefetchedObjects = false fetchRequest.returnsObjectsAsFaults = true @@ -110,7 +115,7 @@ public extension LockManagedObject { #keyPath(EventManagedObject.lock), self ) - return try context.fetch(fetchRequest).first + return try context.fetch(fetchRequest) } } diff --git a/Xcode/LockKit/Model/CoreData/RemoveKeyEventManagedObject.swift b/Xcode/LockKit/Model/CoreData/RemoveKeyEventManagedObject.swift index 31034040..f93a9b8e 100644 --- a/Xcode/LockKit/Model/CoreData/RemoveKeyEventManagedObject.swift +++ b/Xcode/LockKit/Model/CoreData/RemoveKeyEventManagedObject.swift @@ -12,7 +12,7 @@ import CoreLock public final class RemoveKeyEventManagedObject: EventManagedObject { - @nonobjc override class var eventType: LockEvent.EventType { return .removeKey } + @nonobjc override public class var eventType: LockEvent.EventType { return .removeKey } internal convenience init(_ value: LockEvent.RemoveKey, lock: LockManagedObject, context: NSManagedObjectContext) { diff --git a/Xcode/LockKit/Model/CoreData/SetupEventManagedObject.swift b/Xcode/LockKit/Model/CoreData/SetupEventManagedObject.swift index a4ae68f6..44bc69f2 100644 --- a/Xcode/LockKit/Model/CoreData/SetupEventManagedObject.swift +++ b/Xcode/LockKit/Model/CoreData/SetupEventManagedObject.swift @@ -12,7 +12,7 @@ import CoreLock public final class SetupEventManagedObject: EventManagedObject { - @nonobjc override class var eventType: LockEvent.EventType { return .setup } + @nonobjc override public class var eventType: LockEvent.EventType { return .setup } internal convenience init(_ value: LockEvent.Setup, lock: LockManagedObject, context: NSManagedObjectContext) { diff --git a/Xcode/LockKit/Model/CoreData/UnlockEventManagedObject.swift b/Xcode/LockKit/Model/CoreData/UnlockEventManagedObject.swift index c6224dfa..77cb78cd 100644 --- a/Xcode/LockKit/Model/CoreData/UnlockEventManagedObject.swift +++ b/Xcode/LockKit/Model/CoreData/UnlockEventManagedObject.swift @@ -12,7 +12,7 @@ import CoreLock public final class UnlockEventManagedObject: EventManagedObject { - @nonobjc override class var eventType: LockEvent.EventType { return .unlock } + @nonobjc override public class var eventType: LockEvent.EventType { return .unlock } internal convenience init(_ value: LockEvent.Unlock, lock: LockManagedObject, context: NSManagedObjectContext) { diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index ee722813..2269eec6 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -45,7 +45,7 @@ public final class Store: ObservableObject { internal lazy var fileManager: FileManager.Lock = .shared - internal lazy var persistentContainer: NSPersistentContainer = .lock + public lazy var persistentContainer: NSPersistentContainer = .lock public lazy var managedObjectContext: NSManagedObjectContext = { let context = self.persistentContainer.viewContext @@ -55,7 +55,7 @@ public final class Store: ObservableObject { return context }() - internal lazy var backgroundContext: NSManagedObjectContext = { + public lazy var backgroundContext: NSManagedObjectContext = { let context = self.persistentContainer.newBackgroundContext() context.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump context.undoManager = nil diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 35fd5811..90bac6c0 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -8,6 +8,9 @@ /* Begin PBXBuildFile section */ 6E10C90028DFC6C400703691 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C8FF28DFC6C400703691 /* ErrorView.swift */; }; + 6E10C90228DFCFED00703691 /* LockEventEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C90128DFCFED00703691 /* LockEventEntity.swift */; }; + 6E10C90428DFD18D00703691 /* EventTypeAppEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C90328DFD18D00703691 /* EventTypeAppEnum.swift */; }; + 6E10C90628DFD26300703691 /* LockEventQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C90528DFD26300703691 /* LockEventQuery.swift */; }; 6E169A7828D9A34F008545EC /* NavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7728D9A34F008545EC /* NavigationLink.swift */; }; 6E169A7D28D9C097008545EC /* PermissionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7C28D9C097008545EC /* PermissionsView.swift */; }; 6E169A7F28D9C135008545EC /* NewPermissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7E28D9C135008545EC /* NewPermissionView.swift */; }; @@ -153,6 +156,9 @@ /* Begin PBXFileReference section */ 6E10C8FF28DFC6C400703691 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; + 6E10C90128DFCFED00703691 /* LockEventEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockEventEntity.swift; sourceTree = ""; }; + 6E10C90328DFD18D00703691 /* EventTypeAppEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTypeAppEnum.swift; sourceTree = ""; }; + 6E10C90528DFD26300703691 /* LockEventQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockEventQuery.swift; sourceTree = ""; }; 6E169A7728D9A34F008545EC /* NavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationLink.swift; sourceTree = ""; }; 6E169A7C28D9C097008545EC /* PermissionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsView.swift; sourceTree = ""; }; 6E169A7E28D9C135008545EC /* NewPermissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPermissionView.swift; sourceTree = ""; }; @@ -473,6 +479,7 @@ children = ( 6E8BBFFB28DD438500F03735 /* LockEntity.swift */, 6E84E52528DDB0F2008CAE85 /* KeyEntity.swift */, + 6E10C90128DFCFED00703691 /* LockEventEntity.swift */, ); path = AppEntity; sourceTree = ""; @@ -492,6 +499,7 @@ children = ( 6E8BBFFD28DD491100F03735 /* LockQuery.swift */, 6E84E52F28DDCF4F008CAE85 /* KeyQuery.swift */, + 6E10C90528DFD26300703691 /* LockEventQuery.swift */, ); path = EntityQuery; sourceTree = ""; @@ -502,6 +510,7 @@ 6E84E53E28DE86A1008CAE85 /* LockStatusAppEnum.swift */, 6E84E54028DE8728008CAE85 /* UnlockActionAppEnum.swift */, 6E84E54228DE878D008CAE85 /* PermissionAppEnum.swift */, + 6E10C90328DFD18D00703691 /* EventTypeAppEnum.swift */, ); path = AppEnum; sourceTree = ""; @@ -805,6 +814,8 @@ files = ( 6E2182F128D7B4FA00A622B3 /* SettingsView.swift in Sources */, 6E84E52D28DDC841008CAE85 /* LockEntity.swift in Sources */, + 6E10C90228DFCFED00703691 /* LockEventEntity.swift in Sources */, + 6E10C90428DFD18D00703691 /* EventTypeAppEnum.swift in Sources */, 6E84E52728DDC841008CAE85 /* ScanLocksIntent.swift in Sources */, 6E84E54628DEA0CE008CAE85 /* FetchEventsIntent.swift in Sources */, 6E84E52A28DDC841008CAE85 /* Shortcuts.swift in Sources */, @@ -822,6 +833,7 @@ 6E3276D028D70C9400AF171B /* NearbyDevicesView.swift in Sources */, 6E84E52C28DDC841008CAE85 /* LockQuery.swift in Sources */, 6E21831F28D84A7300A622B3 /* SidebarLabel.swift in Sources */, + 6E10C90628DFD26300703691 /* LockEventQuery.swift in Sources */, 6E84E54128DE8728008CAE85 /* UnlockActionAppEnum.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; From 8eebdf6d68b0c8b2dee2d5e96ff3f5c6f1d0f160 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 24 Sep 2022 19:41:27 -0700 Subject: [PATCH 155/229] [App] Added `LockEvent.Predicate.toPredicate()` --- .../Model/CoreData/EventManagedObject.swift | 32 +++++++++++++++++++ Xcode/LockKit/Model/Predicate.swift | 23 ------------- Xcode/LockKit/View/EventsView.swift | 14 ++++---- Xcode/LockKit/View/LockDetailView.swift | 2 +- Xcode/LockKit/View/NavigationLink.swift | 6 ++-- Xcode/SmartLock.xcodeproj/project.pbxproj | 4 --- Xcode/SmartLock/View/SidebarView.swift | 16 ++++++---- 7 files changed, 53 insertions(+), 44 deletions(-) delete mode 100644 Xcode/LockKit/Model/Predicate.swift diff --git a/Xcode/LockKit/Model/CoreData/EventManagedObject.swift b/Xcode/LockKit/Model/CoreData/EventManagedObject.swift index 94fdecc0..3b88b037 100644 --- a/Xcode/LockKit/Model/CoreData/EventManagedObject.swift +++ b/Xcode/LockKit/Model/CoreData/EventManagedObject.swift @@ -150,3 +150,35 @@ internal extension NSManagedObjectContext { return try insert(event, for: managedObject) } } + +// MARK: - Predicate + +public extension LockEvent.Predicate { + + func toFoundation(lock: UUID? = nil) -> NSPredicate { + // CoreData predicate + var subpredicates = [Predicate]() + if let lock = lock { + subpredicates.append( + #keyPath(EventManagedObject.lock.identifier) == lock + ) + } + if let keys = self.keys, keys.isEmpty == false { + subpredicates.append( + #keyPath(EventManagedObject.key).in(keys) + ) + } + if let start = self.start { + subpredicates.append( + #keyPath(EventManagedObject.date) >= start + ) + } + if let end = self.end { + subpredicates.append( + #keyPath(EventManagedObject.date) <= end + ) + } + let predicate: Predicate = subpredicates.isEmpty ? .value(true) : .compound(.and(subpredicates)) + return predicate.toFoundation() + } +} diff --git a/Xcode/LockKit/Model/Predicate.swift b/Xcode/LockKit/Model/Predicate.swift deleted file mode 100644 index 74b18677..00000000 --- a/Xcode/LockKit/Model/Predicate.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// Predicate.swift -// LockKit -// -// Created by Alsey Coleman Miller on 9/21/22. -// - -import Foundation -import CoreData -import Predicate - -public extension LockEvent.Predicate { - - func toFoundation() -> NSPredicate { - // CoreData predicate - var subpredicates = [Predicate]() - if let keys = self.keys, keys.isEmpty == false { - - } - let predicate: Predicate = subpredicates.isEmpty ? .value(true) : .compound(.and(subpredicates)) - return predicate.toFoundation() - } -} diff --git a/Xcode/LockKit/View/EventsView.swift b/Xcode/LockKit/View/EventsView.swift index 9d545cdd..a742db8e 100644 --- a/Xcode/LockKit/View/EventsView.swift +++ b/Xcode/LockKit/View/EventsView.swift @@ -18,7 +18,8 @@ public struct EventsView: View { @Environment(\.managedObjectContext) public var managedObjectContext - // FIXME: Filter by lock as well + @State + public var lock: UUID? @State public var predicate: LockEvent.Predicate? @@ -53,15 +54,16 @@ public struct EventsView: View { @State private var reloadTask: TaskQueue.PendingTask? - public init(predicate: LockEvent.Predicate? = nil) { + public init(lock: UUID?, predicate: LockEvent.Predicate? = nil) { self.predicate = predicate + self.lock = lock } public var body: some View { list .navigationTitle("History") .onAppear { - self._events.wrappedValue.nsPredicate = self.predicate?.toFoundation() + self._events.wrappedValue.nsPredicate = self.predicate?.toFoundation(lock: lock) } #if os(iOS) .toolbar { @@ -164,14 +166,12 @@ private extension EventsView { } func row(for managedObject: EventManagedObject) -> some View { - let context = self.managedObjectContext let eventType = type(of: managedObject).eventType let displayLockName = (self.predicate?.keys?.count ?? 0) == 1 let (action, keyName, _) = try! managedObject.displayRepresentation( displayLockName: displayLockName, in: managedObjectContext ) - return LockRowView( image: .emoji(eventType.symbol), title: action, @@ -217,11 +217,11 @@ struct EventsView_Previews: PreviewProvider { var body: some View { #if os(iOS) NavigationView { - EventsView(predicate: predicate) + EventsView(lock: nil, predicate: predicate) } #else NavigationStack { - EventsView(predicate: predicate) + EventsView(lock: nil, predicate: predicate) } #endif } diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index d6e94ef7..33070603 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -336,7 +336,7 @@ extension LockDetailView { .frame(width: titleWidth, height: nil, alignment: .leading) .font(.body) .foregroundColor(.gray) - AppNavigationLink(id: .events(.init(keys: [cache.key.id])), label: { + AppNavigationLink(id: .events(id, nil), label: { HStack { Text("\(events) events") Image(systemName: "chevron.right") diff --git a/Xcode/LockKit/View/NavigationLink.swift b/Xcode/LockKit/View/NavigationLink.swift index 9477a4f0..fea5bac0 100644 --- a/Xcode/LockKit/View/NavigationLink.swift +++ b/Xcode/LockKit/View/NavigationLink.swift @@ -54,7 +54,7 @@ private extension AppNavigationLink { public enum AppNavigationLinkID: Hashable { case lock(UUID) - case events(LockEvent.Predicate?) + case events(UUID, LockEvent.Predicate?) case permissions(UUID) case key(KeyDetailView.Value) case keySchedule(Permission.Schedule) // view only @@ -101,9 +101,9 @@ public struct AppNavigationDestinationView: View { AnyView( LockDetailView(id: id) ) - case let .events(predicate): + case let .events(lock, predicate): AnyView( - EventsView(predicate: predicate) + EventsView(lock: lock, predicate: predicate) ) case let .permissions(id): AnyView( diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 90bac6c0..d5746038 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -84,7 +84,6 @@ 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62228D7907E00116573 /* PermissionIconViewNSView.swift */; }; 6E4CB62528D792BF00116573 /* InfoPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62428D792BF00116573 /* InfoPlist.swift */; }; 6E6A97EF28DBEC2D00C689F6 /* NewKeyInvitationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */; }; - 6E6A97F128DBEE0700C689F6 /* Predicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97F028DBEE0700C689F6 /* Predicate.swift */; }; 6E6A97F328DC030000C689F6 /* NewKeyInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */; }; 6E84E52728DDC841008CAE85 /* ScanLocksIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFEC28DD301B00F03735 /* ScanLocksIntent.swift */; }; 6E84E52828DDC841008CAE85 /* UnlockIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFFF28DD492300F03735 /* UnlockIntent.swift */; }; @@ -230,7 +229,6 @@ 6E4CB62228D7907E00116573 /* PermissionIconViewNSView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionIconViewNSView.swift; sourceTree = ""; }; 6E4CB62428D792BF00116573 /* InfoPlist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InfoPlist.swift; path = ../../../iOS/SmartLock/InfoPlist.swift; sourceTree = ""; }; 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyInvitationStore.swift; sourceTree = ""; }; - 6E6A97F028DBEE0700C689F6 /* Predicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Predicate.swift; sourceTree = ""; }; 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyInvitationView.swift; sourceTree = ""; }; 6E84E51F28DD9646008CAE85 /* Shortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shortcuts.swift; sourceTree = ""; }; 6E84E52528DDB0F2008CAE85 /* KeyEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyEntity.swift; sourceTree = ""; }; @@ -417,7 +415,6 @@ 6E21831228D80FDD00A622B3 /* JSON.swift */, 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */, 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */, - 6E6A97F028DBEE0700C689F6 /* Predicate.swift */, 6E84E53528DDE01F008CAE85 /* AssetExtractor.swift */, 6E21830F28D80DCD00A622B3 /* Keychain.swift */, 6E21831628D8116C00A622B3 /* NewKey.swift */, @@ -911,7 +908,6 @@ 6E21833928D8F3B500A622B3 /* ScheduleManagedObject.swift in Sources */, 6E21836028D9516B00A622B3 /* CloudEvent.swift in Sources */, 6E21831328D80FDD00A622B3 /* JSON.swift in Sources */, - 6E6A97F128DBEE0700C689F6 /* Predicate.swift in Sources */, 6E21832128D8501500A622B3 /* ProgressIndicatorView.swift in Sources */, 6E21833828D8F3B500A622B3 /* NewKeyManagedObject.swift in Sources */, 6E4CB61B28D788AA00116573 /* PermissionIconView.swift in Sources */, diff --git a/Xcode/SmartLock/View/SidebarView.swift b/Xcode/SmartLock/View/SidebarView.swift index 533ff7c7..d46c5ba1 100644 --- a/Xcode/SmartLock/View/SidebarView.swift +++ b/Xcode/SmartLock/View/SidebarView.swift @@ -157,8 +157,8 @@ private extension SidebarView { return } detail = AnyView(detailView(for: lock)) - case let .events(predicate, _): - detail = AnyView(EventsView(predicate: predicate)) + case let .events(lock, predicate, _): + detail = AnyView(EventsView(lock: lock, predicate: predicate)) } } @@ -315,7 +315,7 @@ extension SidebarView { case lock(NativePeripheral.ID, String, PermissionType?) case key(UUID, String, PermissionType) //case newKey(URL) - case events(LockEvent.Predicate?, String) + case events(UUID?, LockEvent.Predicate?, String) } } @@ -327,8 +327,12 @@ extension SidebarView.Item: Identifiable { return "lock_" + id.description case let .key(id, _, _): return "key_" + id.description - case let .events(predicate, _): - return "events_\(predicate.flatMap { "\($0)" } ?? "all")" + case let .events(lock, predicate, _): + if lock == nil, predicate == nil { + return "event_\(lock?.description ?? "")\(predicate.map { String(describing: $0) } ?? "")" + } else { + return "events_all" + } } } } @@ -347,7 +351,7 @@ extension SidebarLabel { title: title, image: .permission(permission) ) - case let .events(_, name): + case let .events(_, _, name): self.init(title: name, image: .symbol(.clock)) } } From 08ecc89ce4fefba5f35b537cf447ca46230117a1 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 24 Sep 2022 19:41:50 -0700 Subject: [PATCH 156/229] [App] Added `LockEventEntity.image` --- .../AppEntity/LockEventEntity.swift | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/Xcode/LockIntents/AppEntity/LockEventEntity.swift b/Xcode/LockIntents/AppEntity/LockEventEntity.swift index ff00c62d..e9abfa39 100644 --- a/Xcode/LockIntents/AppEntity/LockEventEntity.swift +++ b/Xcode/LockIntents/AppEntity/LockEventEntity.swift @@ -7,9 +7,16 @@ import Foundation import CoreData +import SwiftUI import AppIntents import LockKit +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif + /// Lock Intent Entity @available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) struct LockEventEntity: AppEntity, Identifiable { @@ -46,7 +53,7 @@ extension LockEventEntity { return DisplayRepresentation( title: "\(title)", subtitle: "\(subtitle)\n\(date.formatted(date: .numeric, time: .shortened))", - image: nil + image: image ) } catch { assertionFailure("Unable to fetch event. \(error)") @@ -62,9 +69,29 @@ private extension LockEventEntity { DisplayRepresentation( title: "\(String(type.symbol)) \(type.localizedStringResource)", // - \(lock.name ?? "Lock")", subtitle: "\(date.formatted(date: .abbreviated, time: .shortened))", - image: nil + image: image ) } + + var image: DisplayRepresentation.Image? { + return DispatchQueue.main.sync { + let view = Text(verbatim: String(type.symbol)) + .font(.system(size: 43)) + let imageRenderer = ImageRenderer(content: view) + #if canImport(UIKit) + return imageRenderer.uiImage? + .pngData() + .map { .init(data: $0) } + #elseif canImport(AppKit) + return imageRenderer.cgImage + .map { NSBitmapImageRep(cgImage: $0) }? + .representation(using: .png, properties: [:]) + .map { .init(data: $0) } + #else + return nil + #endif + } + } } @available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) From f61c78846c915b246230a39f4e8e5806f03b0c05 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 24 Sep 2022 20:09:11 -0700 Subject: [PATCH 157/229] [App] Added `FetchEventsIntent` view --- .../AppIntent/FetchEventsIntent.swift | 76 ++++++++++++++++--- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift b/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift index 5d689a8c..a90dd5f3 100644 --- a/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift +++ b/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift @@ -5,7 +5,9 @@ // Created by Alsey Coleman Miller on 9/23/22. // +import Foundation import AppIntents +import CoreData import SwiftUI import LockKit @@ -70,28 +72,78 @@ struct FetchEventsIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { - let limit = min(self.limit, Int(UInt8.max)) let store = Store.shared - let fetchRequest = LockEvent.FetchRequest( - offset: UInt8(offset), - limit: UInt8(limit), - predicate: .init( - keys: keys.map { $0.id }, - start: start, - end: end - ) - ) // search for lock if not in cache guard let peripheral = try await store.device(for: lock.id) else { throw LockError.notInRange(lock: lock.id) } // fetch events - let events = try await Store.shared.listEvents( + let events = try await store.listEvents( for: peripheral, fetchRequest: fetchRequest ) + let managedObjectContext = Store.shared.managedObjectContext + let managedObjects = try events.compactMap { try EventManagedObject.find($0.id, in: managedObjectContext) } + assert(managedObjects.count == events.count) return .result( - value: events.map { LockEventEntity($0) } + value: events.map { LockEventEntity($0) }, + view: view(for: managedObjects, in: managedObjectContext) + ) + } +} + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +@MainActor +private extension FetchEventsIntent { + + var fetchRequest: LockEvent.FetchRequest { + LockEvent.FetchRequest( + offset: UInt8(offset), + limit: UInt8(min(self.limit, Int(UInt8.max))), + predicate: .init( + keys: keys.map { $0.id }, + start: start, + end: end + ) + ) + } + + func view(for results: [EventManagedObject], in managedObjectContext: NSManagedObjectContext) -> some View { + HStack(alignment: .top, spacing: 0) { + VStack(alignment: .leading, spacing: 8) { + if results.isEmpty { + Text("No events found.") + .padding(20) + } else { + if results.count > 3 { + Text("Found \(results.count) events.") + .padding(20) + } else { + ForEach(results) { + view(for: $0, in: managedObjectContext) + .padding(8) + } + } + } + } + Spacer(minLength: 0) + } + } + + func view(for managedObject: EventManagedObject, in managedObjectContext: NSManagedObjectContext) -> some View { + let eventType = type(of: managedObject).eventType + let (action, keyName, _) = try! managedObject.displayRepresentation( + displayLockName: true, + in: managedObjectContext + ) + return LockRowView( + image: .emoji(eventType.symbol), + title: action, + subtitle: keyName, + trailing: ( + managedObject.date?.formatted(date: .abbreviated, time: .omitted) ?? "", + managedObject.date?.formatted(date: .omitted, time: .shortened) ?? "" + ) ) } } From 5151781cb5ecfc3145a6e419ebc81f584b3b4401 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 24 Sep 2022 20:09:39 -0700 Subject: [PATCH 158/229] [App] Fixed `EventsView.init()` --- Xcode/LockKit/View/EventsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Xcode/LockKit/View/EventsView.swift b/Xcode/LockKit/View/EventsView.swift index a742db8e..bf636cfd 100644 --- a/Xcode/LockKit/View/EventsView.swift +++ b/Xcode/LockKit/View/EventsView.swift @@ -54,7 +54,7 @@ public struct EventsView: View { @State private var reloadTask: TaskQueue.PendingTask? - public init(lock: UUID?, predicate: LockEvent.Predicate? = nil) { + public init(lock: UUID? = nil, predicate: LockEvent.Predicate? = nil) { self.predicate = predicate self.lock = lock } From 16b0dfe28a5479415ffd4b1f3cccd17113e233fb Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 24 Sep 2022 23:04:24 -0700 Subject: [PATCH 159/229] [App] Added `toHexadecimal()` extension --- Sources/CoreLock/Extensions/Hexadecimal.swift | 31 +++++++++++++++++++ Xcode/LockKit/Extensions/Hexadecimal.swift | 31 +++++++++++++++++++ Xcode/SmartLock.xcodeproj/project.pbxproj | 4 +++ 3 files changed, 66 insertions(+) create mode 100644 Sources/CoreLock/Extensions/Hexadecimal.swift create mode 100644 Xcode/LockKit/Extensions/Hexadecimal.swift diff --git a/Sources/CoreLock/Extensions/Hexadecimal.swift b/Sources/CoreLock/Extensions/Hexadecimal.swift new file mode 100644 index 00000000..528ae4b0 --- /dev/null +++ b/Sources/CoreLock/Extensions/Hexadecimal.swift @@ -0,0 +1,31 @@ +// +// Hexadecimal.swift +// Bluetooth +// +// Created by Alsey Coleman Miller on 3/2/16. +// Copyright © 2016 PureSwift. All rights reserved. +// + +internal extension FixedWidthInteger { + + func toHexadecimal() -> String { + + var string = String(self, radix: 16) + while string.utf8.count < (MemoryLayout.size * 2) { + string = "0" + string + } + return string.uppercased() + } +} + +internal extension Collection where Element: FixedWidthInteger { + + func toHexadecimal() -> String { + let length = count * MemoryLayout.size * 2 + var string = "" + string.reserveCapacity(length) + string = reduce(into: string) { $0 += $1.toHexadecimal() } + assert(string.count == length) + return string + } +} diff --git a/Xcode/LockKit/Extensions/Hexadecimal.swift b/Xcode/LockKit/Extensions/Hexadecimal.swift new file mode 100644 index 00000000..528ae4b0 --- /dev/null +++ b/Xcode/LockKit/Extensions/Hexadecimal.swift @@ -0,0 +1,31 @@ +// +// Hexadecimal.swift +// Bluetooth +// +// Created by Alsey Coleman Miller on 3/2/16. +// Copyright © 2016 PureSwift. All rights reserved. +// + +internal extension FixedWidthInteger { + + func toHexadecimal() -> String { + + var string = String(self, radix: 16) + while string.utf8.count < (MemoryLayout.size * 2) { + string = "0" + string + } + return string.uppercased() + } +} + +internal extension Collection where Element: FixedWidthInteger { + + func toHexadecimal() -> String { + let length = count * MemoryLayout.size * 2 + var string = "" + string.reserveCapacity(length) + string = reduce(into: string) { $0 += $1.toHexadecimal() } + assert(string.count == length) + return string + } +} diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index d5746038..7a497351 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 6E10C90228DFCFED00703691 /* LockEventEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C90128DFCFED00703691 /* LockEventEntity.swift */; }; 6E10C90428DFD18D00703691 /* EventTypeAppEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C90328DFD18D00703691 /* EventTypeAppEnum.swift */; }; 6E10C90628DFD26300703691 /* LockEventQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C90528DFD26300703691 /* LockEventQuery.swift */; }; + 6E10C90828E0006700703691 /* Hexadecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C90728E0006700703691 /* Hexadecimal.swift */; }; 6E169A7828D9A34F008545EC /* NavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7728D9A34F008545EC /* NavigationLink.swift */; }; 6E169A7D28D9C097008545EC /* PermissionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7C28D9C097008545EC /* PermissionsView.swift */; }; 6E169A7F28D9C135008545EC /* NewPermissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7E28D9C135008545EC /* NewPermissionView.swift */; }; @@ -158,6 +159,7 @@ 6E10C90128DFCFED00703691 /* LockEventEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockEventEntity.swift; sourceTree = ""; }; 6E10C90328DFD18D00703691 /* EventTypeAppEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTypeAppEnum.swift; sourceTree = ""; }; 6E10C90528DFD26300703691 /* LockEventQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockEventQuery.swift; sourceTree = ""; }; + 6E10C90728E0006700703691 /* Hexadecimal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Hexadecimal.swift; sourceTree = ""; }; 6E169A7728D9A34F008545EC /* NavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationLink.swift; sourceTree = ""; }; 6E169A7C28D9C097008545EC /* PermissionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsView.swift; sourceTree = ""; }; 6E169A7E28D9C135008545EC /* NewPermissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPermissionView.swift; sourceTree = ""; }; @@ -337,6 +339,7 @@ isa = PBXGroup; children = ( 6E21834528D8FC4300A622B3 /* Task.swift */, + 6E10C90728E0006700703691 /* Hexadecimal.swift */, 6E21834928D90F7A00A622B3 /* LocalAuthentication.swift */, 6E84E53328DDDCDC008CAE85 /* Bundle.swift */, ); @@ -906,6 +909,7 @@ 6E21830328D7C47500A622B3 /* Appearance.swift in Sources */, 6E3276DC28D7195400AF171B /* Central.swift in Sources */, 6E21833928D8F3B500A622B3 /* ScheduleManagedObject.swift in Sources */, + 6E10C90828E0006700703691 /* Hexadecimal.swift in Sources */, 6E21836028D9516B00A622B3 /* CloudEvent.swift in Sources */, 6E21831328D80FDD00A622B3 /* JSON.swift in Sources */, 6E21832128D8501500A622B3 /* ProgressIndicatorView.swift in Sources */, From e2a8df581c8b35ceb4e33736362db082d0d5a895 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 24 Sep 2022 23:04:41 -0700 Subject: [PATCH 160/229] [CoreLock] Updated `UnlockState` --- Sources/CoreLock/LockState.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/CoreLock/LockState.swift b/Sources/CoreLock/LockState.swift index 27a06bd5..4304353f 100644 --- a/Sources/CoreLock/LockState.swift +++ b/Sources/CoreLock/LockState.swift @@ -12,6 +12,4 @@ public enum UnlockState: UInt8, BitMaskOption { /// Locked. case close = 0b10 - - public static let all: Set = [.open, .close] } From b95332e9dc7b9e33ebbb1fd14c2aa835e47f03c8 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 24 Sep 2022 23:05:21 -0700 Subject: [PATCH 161/229] [App] Updated Mock data --- Xcode/LockKit/Model/Mock/MockAttributes.swift | 38 ++++ Xcode/LockKit/Model/Mock/MockCentral.swift | 109 ++++++++-- Xcode/LockKit/Model/Mock/MockLock.swift | 201 +++++++++--------- Xcode/LockKit/Model/Mock/MockScanData.swift | 6 +- 4 files changed, 235 insertions(+), 119 deletions(-) diff --git a/Xcode/LockKit/Model/Mock/MockAttributes.swift b/Xcode/LockKit/Model/Mock/MockAttributes.swift index 244299b6..23f7ccc3 100644 --- a/Xcode/LockKit/Model/Mock/MockAttributes.swift +++ b/Xcode/LockKit/Model/Mock/MockAttributes.swift @@ -6,9 +6,11 @@ // #if DEBUG +import Foundation import SwiftUI import Bluetooth import GATT +import CoreLock typealias MockService = GATT.Service typealias MockCharacteristic = GATT.Characteristic @@ -111,6 +113,42 @@ extension MockCharacteristic { properties: [.read] ) } + + static func lockEventsRequest(_ id: UInt8) -> MockCharacteristic { + Characteristic( + id: 42, + uuid: ListEventsCharacteristic.uuid, + peripheral: .lock(id), + properties: ListEventsCharacteristic.properties + ) + } + + static func lockEventsNotifications(_ id: UInt8) -> MockCharacteristic { + Characteristic( + id: 43, + uuid: EventsCharacteristic.uuid, + peripheral: .lock(id), + properties: EventsCharacteristic.properties + ) + } + + static func lockKeysRequest(_ id: UInt8) -> MockCharacteristic { + Characteristic( + id: 44, + uuid: ListKeysCharacteristic.uuid, + peripheral: .lock(id), + properties: ListKeysCharacteristic.properties + ) + } + + static func lockKeysNotifications(_ id: UInt8) -> MockCharacteristic { + Characteristic( + id: 45, + uuid: KeysCharacteristic.uuid, + peripheral: .lock(id), + properties: KeysCharacteristic.properties + ) + } } extension MockDescriptor { diff --git a/Xcode/LockKit/Model/Mock/MockCentral.swift b/Xcode/LockKit/Model/Mock/MockCentral.swift index adf2233d..9b35a668 100644 --- a/Xcode/LockKit/Model/Mock/MockCentral.swift +++ b/Xcode/LockKit/Model/Mock/MockCentral.swift @@ -10,6 +10,7 @@ import Foundation import Bluetooth import GATT import DarwinGATT +import CoreLock public final class MockCentral: CentralManager { @@ -28,14 +29,14 @@ public final class MockCentral: CentralManager { public var state: DarwinBluetoothState { get async { - try? await Task.sleep(timeInterval: 0.1) + try? await Task.sleep(timeInterval: 0.01) return await storage.bluetoothState } } public var peripherals: Set { get async { - try? await Task.sleep(timeInterval: 0.1) + try? await Task.sleep(timeInterval: 0.01) return await Set(storage.state.scanData.map { $0.peripheral }) } } @@ -81,6 +82,7 @@ public final class MockCentral: CentralManager { } } try await Task.sleep(timeInterval: 0.2) + var count = 0 for scanData in await self.storage.state.scanData { // apply filter if services.isEmpty == false { @@ -94,12 +96,15 @@ public final class MockCentral: CentralManager { continue } continuation(scanData) + count += 1 } + self.log?("Discovered \(count) peripherals") } } /// Connect to the specified device public func connect(to peripheral: Peripheral) async throws { + log?("Will connect to \(peripheral)") let state = await self.state guard state == .poweredOn else { throw DarwinCentralError.invalidState(state) @@ -113,13 +118,16 @@ public final class MockCentral: CentralManager { public func disconnect(_ peripheral: Peripheral) { Task { await self.storage.updateState { - $0.connected.remove(peripheral) + if $0.connected.remove(peripheral) != nil { + self.log?("Will disconnect \(peripheral)") + } } } } /// Disconnect all connected devices. public func disconnectAll() { + self.log?("Will disconnect all") Task { await storage.updateState { $0.connected.removeAll() @@ -132,6 +140,7 @@ public final class MockCentral: CentralManager { _ services: Set = [], for peripheral: Peripheral ) async throws -> [Service] { + log?("Peripheral \(peripheral) will discover services") let state = await self.state guard state == .poweredOn else { throw DarwinCentralError.invalidState(state) @@ -147,6 +156,7 @@ public final class MockCentral: CentralManager { _ services: Set = [], for service: Service ) async throws -> [Service] { + log?("Peripheral \(service.peripheral) will discover included services of service \(service.uuid)") let state = await self.state guard state == .poweredOn else { throw DarwinCentralError.invalidState(state) @@ -159,6 +169,7 @@ public final class MockCentral: CentralManager { _ characteristics: Set = [], for service: Service ) async throws -> [Characteristic] { + log?("Peripheral \(service.peripheral) will discover characteristics of service \(service.uuid)") let state = await self.state guard state == .poweredOn else { throw DarwinCentralError.invalidState(state) @@ -177,6 +188,11 @@ public final class MockCentral: CentralManager { public func readValue( for characteristic: Characteristic ) async throws -> Data { + log?("Peripheral \(characteristic.peripheral) will read characteristic \(characteristic.uuid)") + let state = await self.state + guard state == .poweredOn else { + throw DarwinCentralError.invalidState(state) + } guard await storage.state.connected.contains(characteristic.peripheral) else { throw CentralError.disconnected } @@ -189,6 +205,7 @@ public final class MockCentral: CentralManager { for characteristic: Characteristic, withResponse: Bool = true ) async throws { + log?("Peripheral \(characteristic.peripheral) will write characteristic \(characteristic.uuid)") let state = await self.state guard state == .poweredOn else { throw DarwinCentralError.invalidState(state) @@ -209,12 +226,57 @@ public final class MockCentral: CentralManager { await storage.updateState { $0.characteristicValues[characteristic] = data } + // mock + guard let (index, lock) = MockLock.locks + .enumerated() + .first(where: { Peripheral.lock(UInt8($0.offset)) == characteristic.peripheral }) + else { return } + switch characteristic.uuid { + case ListEventsCharacteristic.uuid: + guard let listEventsCharacteristic = ListEventsCharacteristic(data: data) else { + throw CentralError.invalidAttribute(ListEventsCharacteristic.uuid) + } + let key = listEventsCharacteristic.encryptedData.authentication.message.id + guard let keyData = await Store.shared[key: key] else { + throw CentralError.invalidAttribute(ListEventsCharacteristic.uuid) + } + let request = try listEventsCharacteristic.decrypt(using: keyData) + let events = request.fetchRequest.map { lock.events.fetch($0) } ?? lock.events + // build notifications + await storage.updateState { + $0.notifications[.lockEventsNotifications(UInt8(index))] = EventListNotification + .from(list: events) + .reduce([], { try! $0 + EventsCharacteristic.from($1, id: key, key: keyData, maximumUpdateValueLength: 20) }) + .map { $0.data } + } + case ListKeysCharacteristic.uuid: + guard let listKeysCharacteristic = ListKeysCharacteristic(data: data) else { + throw CentralError.invalidAttribute(ListKeysCharacteristic.uuid) + } + let key = listKeysCharacteristic.authentication.message.id + guard let keyData = await Store.shared[key: key] else { + throw CentralError.invalidAttribute(ListKeysCharacteristic.uuid) + } + let keysList = KeysList( + keys: lock.keys, + newKeys: lock.newKeys + ) + await storage.updateState { + $0.notifications[.lockEventsNotifications(UInt8(index))] = KeyListNotification + .from(list: keysList) + .reduce([], { try! $0 + KeysCharacteristic.from($1, id: key, key: keyData, maximumUpdateValueLength: 20) }) + .map { $0.data } + } + default: + return + } } /// Discover descriptors public func discoverDescriptors( for characteristic: Characteristic ) async throws -> [Descriptor] { + log?("Peripheral \(characteristic.peripheral) will discover descriptors of characteristic \(characteristic.uuid)") let state = await self.state guard state == .poweredOn else { throw DarwinCentralError.invalidState(state) @@ -229,6 +291,7 @@ public final class MockCentral: CentralManager { public func readValue( for descriptor: Descriptor ) async throws -> Data { + log?("Peripheral \(descriptor.peripheral) will read descriptor \(descriptor.uuid)") let state = await self.state guard state == .poweredOn else { throw DarwinCentralError.invalidState(state) @@ -244,6 +307,7 @@ public final class MockCentral: CentralManager { _ data: Data, for descriptor: Descriptor ) async throws { + log?("Peripheral \(descriptor.peripheral) will write descriptor \(descriptor.uuid)") let state = await self.state guard state == .poweredOn else { throw DarwinCentralError.invalidState(state) @@ -259,6 +323,7 @@ public final class MockCentral: CentralManager { public func notify( for characteristic: GATT.Characteristic ) async throws -> AsyncCentralNotifications { + log?("Peripheral \(characteristic.peripheral) will enable notifications for characteristic \(characteristic.uuid)") let state = await self.state guard state == .poweredOn else { throw DarwinCentralError.invalidState(state) @@ -267,9 +332,10 @@ public final class MockCentral: CentralManager { throw CentralError.disconnected } return AsyncCentralNotifications { [unowned self] continuation in + try await Task.sleep(nanoseconds: 100_000_000) if let notifications = await storage.state.notifications[characteristic] { for notification in notifications { - try await Task.sleep(nanoseconds: 100_000_000) + try await Task.sleep(nanoseconds: 100_000) continuation(notification) } } @@ -278,6 +344,7 @@ public final class MockCentral: CentralManager { /// Read MTU public func maximumTransmissionUnit(for peripheral: Peripheral) async throws -> MaximumTransmissionUnit { + self.log?("Will read MTU for \(peripheral)") let state = await self.state guard state == .poweredOn else { throw DarwinCentralError.invalidState(state) @@ -290,6 +357,7 @@ public final class MockCentral: CentralManager { // Read RSSI public func rssi(for peripheral: Peripheral) async throws -> RSSI { + log?("Will read RSSI for \(peripheral)") let state = await self.state guard state == .poweredOn else { throw DarwinCentralError.invalidState(state) @@ -334,7 +402,11 @@ internal extension MockCentral { let characteristics = MockLock.locks.enumerated().map { (index, lock) in let id = UInt8(index) return (MockService.lock(id), [ - Characteristic.lockInformation(id) + MockCharacteristic.lockInformation(id), + MockCharacteristic.lockEventsRequest(id), + MockCharacteristic.lockEventsNotifications(id), + MockCharacteristic.lockKeysRequest(id), + MockCharacteristic.lockKeysNotifications(id) ]) } return .init(uniqueKeysWithValues: characteristics) @@ -343,15 +415,22 @@ internal extension MockCentral { .batteryLevel: [.clientCharacteristicConfiguration(.beacon)], .savantTest: [.clientCharacteristicConfiguration(.smartThermostat)], ] - var characteristicValues: [MockCharacteristic: Data] = .init(uniqueKeysWithValues: MockLock.locks.enumerated().map({ (index, lock) in - (.lockInformation(UInt8(index)), LockInformationCharacteristic( - id: lock.id, - buildVersion: .current, - version: .current, - status: lock.status, - unlockActions: [.default] - ).data) - })) + var characteristicValues: [MockCharacteristic: Data] = { + var values = [MockCharacteristic: Data]() + for (index, lock) in MockLock.locks.enumerated() { + values[.lockInformation(UInt8(index))] = LockInformationCharacteristic( + id: lock.id, + buildVersion: .current, + version: .current, + status: lock.status, + unlockActions: [.default] + ).data + values[.lockEventsRequest(UInt8(index))] = Data() + values[.lockKeysRequest(UInt8(index))] = Data() + } + return values + }() + //.init(uniqueKeysWithValues: MockLock.locks.enumerated().reduce([(MockCharacteristic, Data)](), { (index, lock) in var descriptorValues: [MockDescriptor: Data] = [ .clientCharacteristicConfiguration(.beacon): Data([0x00]), .clientCharacteristicConfiguration(.smartThermostat): Data([0x00]), @@ -373,7 +452,7 @@ internal extension MockCentral { Data(UUID().uuidString.utf8), Data(UUID().uuidString.utf8), Data(UUID().uuidString.utf8), - ] + ], ] } diff --git a/Xcode/LockKit/Model/Mock/MockLock.swift b/Xcode/LockKit/Model/Mock/MockLock.swift index a4d3a0cc..46b095b2 100644 --- a/Xcode/LockKit/Model/Mock/MockLock.swift +++ b/Xcode/LockKit/Model/Mock/MockLock.swift @@ -9,15 +9,19 @@ import Foundation import CoreLock #if DEBUG -public struct MockLock: Equatable, Hashable, Codable, Identifiable { +public struct MockLock: Equatable, Codable, Identifiable { - public let id: UUID + public var id: UUID = UUID() - public var status: LockStatus + public var status: LockStatus = .unlock - public var sharedSecret: KeyData + public var sharedSecret: KeyData = KeyData() + + public var events: [LockEvent] = [] public var keys: [Key] = [] + + public var newKeys: [NewKey] = [] } internal extension Store { @@ -26,8 +30,8 @@ internal extension Store { let maxLocks = 3 for index in 0 ..< maxLocks { let lock = MockLock.locks[index] - let key = self.applicationData.locks[lock.id]?.key ?? Key( - id: UUID(), + let key = lock.keys.first ?? Key( + id: UUID(uuidString: "53F21D45-2E82-43CC-9FDC-18313511\(UInt16(index).toHexadecimal())")!, name: "Owner", created: Date() - TimeInterval(60 * (maxLocks - index + 1)), permission: .owner @@ -56,109 +60,104 @@ public extension MockLock { static var locks: [MockLock] = [ MockLock( id: UUID(uuidString: "669A06D7-5AE5-431B-971C-7A118E77CA51")!, - status: .unlock, - sharedSecret: KeyData() + events: [ + .setup( + .init( + id: UUID(uuidString: "3CEAD223-2CBF-4216-85CD-CAD79302E235")!, + date: Date() - TimeInterval(60 * (3 - 0 + 1)), + key: UUID(uuidString: "53F21D45-2E82-43CC-9FDC-18313511\(UInt16(0x00).toHexadecimal())")! + ) + ), + .unlock( + .init( + id: UUID(uuidString: "5DC5E7CF-2C34-4876-8BE8-853CAF64BE84")!, + date: Date() - 10, + key: UUID(uuidString: "53F21D45-2E82-43CC-9FDC-18313511\(UInt16(0x00).toHexadecimal())")!, + action: .default + ) + ) + ], + keys: [ + Key( + id: UUID(uuidString: "53F21D45-2E82-43CC-9FDC-18313511\(UInt16(0x00).toHexadecimal())")!, + name: "Owner", + created: Date() - TimeInterval(60 * (3 - 0 + 1)), + permission: .owner + ) + ] ), MockLock( id: UUID(uuidString: "CCAB00A4-A0BE-4D43-B0D6-A9BAB4628256")!, status: .unlock, - sharedSecret: KeyData() + sharedSecret: KeyData(), + events: [ + .setup( + .init( + id: UUID(uuidString: "3CEAD223-2CBF-4216-85CD-CAD79302E201")!, + date: Date() - TimeInterval(60 * (3 - 0 + 1)), + key: UUID(uuidString: "53F21D45-2E82-43CC-9FDC-18313511\(UInt16(0x01).toHexadecimal())")! + ) + ), + .unlock( + .init( + id: UUID(uuidString: "5DC5E7CF-2C34-4876-8BE8-853CAF64BE02")!, + date: Date() - 10, + key: UUID(uuidString: "53F21D45-2E82-43CC-9FDC-18313511\(UInt16(0x01).toHexadecimal())")!, + action: .default + ) + ) + ], + keys: [ + Key( + id: UUID(uuidString: "53F21D45-2E82-43CC-9FDC-18313511\(UInt16(0x01).toHexadecimal())")!, + name: "Owner", + created: Date() - TimeInterval(60 * (3 - 1 + 1)), + permission: .owner + ) + ] ), MockLock( id: UUID(uuidString: "2AF2BFF2-F826-4154-AA61-E2D41C45CF34")!, status: .unlock, - sharedSecret: KeyData() - ), - MockLock( - id: UUID(), - status: .unlock, - sharedSecret: KeyData() - ), - MockLock( - id: UUID(), - status: .unlock, - sharedSecret: KeyData() - ), - MockLock( - id: UUID(), - status: .unlock, - sharedSecret: KeyData() - ), - MockLock( - id: UUID(), - status: .unlock, - sharedSecret: KeyData() - ), - MockLock( - id: UUID(), - status: .unlock, - sharedSecret: KeyData() - ), - MockLock( - id: UUID(), - status: .unlock, - sharedSecret: KeyData() - ), - MockLock( - id: UUID(), - status: .unlock, - sharedSecret: KeyData() - ), - MockLock( - id: UUID(), - status: .unlock, - sharedSecret: KeyData() - ), - MockLock( - id: UUID(), - status: .unlock, - sharedSecret: KeyData() - ), - MockLock( - id: UUID(), - status: .unlock, - sharedSecret: KeyData() - ), - MockLock( - id: UUID(), - status: .unlock, - sharedSecret: KeyData() - ), - MockLock( - id: UUID(), - status: .unlock, - sharedSecret: KeyData() - ), - MockLock( - id: UUID(), - status: .unlock, - sharedSecret: KeyData() - ), - MockLock( - id: UUID(), - status: .unlock, - sharedSecret: KeyData() - ), - MockLock( - id: UUID(), - status: .unlock, - sharedSecret: KeyData() - ), - MockLock( - id: UUID(), - status: .unlock, - sharedSecret: KeyData() - ), - MockLock( - id: UUID(), - status: .unlock, - sharedSecret: KeyData() - ), - MockLock( - id: UUID(), - status: .unlock, - sharedSecret: KeyData() - ), + sharedSecret: KeyData(), + events: [ + .setup( + .init( + id: UUID(uuidString: "3CEAD223-2CBF-4216-85CD-CAD79302E202")!, + date: Date() - TimeInterval(60 * (3 - 0 + 1)), + key: UUID(uuidString: "53F21D45-2E82-43CC-9FDC-18313511\(UInt16(0x02).toHexadecimal())")! + ) + ), + .unlock( + .init( + id: UUID(uuidString: "5DC5E7CF-2C34-4876-8BE8-853CAF64BE02")!, + date: Date() - 10, + key: UUID(uuidString: "53F21D45-2E82-43CC-9FDC-18313511\(UInt16(0x02).toHexadecimal())")!, + action: .default + ) + ) + ], + keys: [ + Key( + id: UUID(uuidString: "53F21D45-2E82-43CC-9FDC-18313511\(UInt16(0x02).toHexadecimal())")!, + name: "Owner", + created: Date() - TimeInterval(60 * (3 - 2 + 1)), + permission: .owner + ) + ] + ), + MockLock(), + MockLock(), + MockLock(), + MockLock(), + MockLock(), + MockLock(), + MockLock(), + MockLock(), + MockLock(), + MockLock(), + MockLock(), + MockLock() ] } #endif diff --git a/Xcode/LockKit/Model/Mock/MockScanData.swift b/Xcode/LockKit/Model/Mock/MockScanData.swift index a9a718b9..87bf4266 100644 --- a/Xcode/LockKit/Model/Mock/MockScanData.swift +++ b/Xcode/LockKit/Model/Mock/MockScanData.swift @@ -42,7 +42,7 @@ public extension MockScanData { } static var lock: MockScanData { - .lock(0x01) + .lock(0x00) } } @@ -61,11 +61,11 @@ public extension MockCentral.Peripheral { } static func lock(_ id: UInt8) -> Peripheral { - Peripheral(id: BluetoothAddress(bytes: (0x00, 0xAA, 0xBB, 0xCC, 0xDD, id))) + Peripheral(id: .init(bigEndian: BluetoothAddress(bytes: (0x00, 0xAA, 0xBB, 0xCC, 0xDD, id)))) } static var lock: Peripheral { - .lock(0x01) + .lock(0x00) } } From 038e81a4e6ad01ea4b125e3ebfa0d1df893460d6 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 24 Sep 2022 23:25:49 -0700 Subject: [PATCH 162/229] [App] Updated mocked data --- Xcode/LockKit/Model/Mock/MockCentral.swift | 66 ++++++++++------------ 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/Xcode/LockKit/Model/Mock/MockCentral.swift b/Xcode/LockKit/Model/Mock/MockCentral.swift index 9b35a668..cc1637b6 100644 --- a/Xcode/LockKit/Model/Mock/MockCentral.swift +++ b/Xcode/LockKit/Model/Mock/MockCentral.swift @@ -243,11 +243,16 @@ public final class MockCentral: CentralManager { let request = try listEventsCharacteristic.decrypt(using: keyData) let events = request.fetchRequest.map { lock.events.fetch($0) } ?? lock.events // build notifications - await storage.updateState { - $0.notifications[.lockEventsNotifications(UInt8(index))] = EventListNotification - .from(list: events) - .reduce([], { try! $0 + EventsCharacteristic.from($1, id: key, key: keyData, maximumUpdateValueLength: 20) }) - .map { $0.data } + let notifications = EventListNotification + .from(list: events) + .reduce([], { try! $0 + EventsCharacteristic.from($1, id: key, key: keyData, maximumUpdateValueLength: 20) }) + .map { $0.data } + guard let continuation = await storage.state.notifications[.lockEventsNotifications(UInt8(index))] else { + assertionFailure() + return + } + notifications.forEach { + continuation.yield($0) } case ListKeysCharacteristic.uuid: guard let listKeysCharacteristic = ListKeysCharacteristic(data: data) else { @@ -261,11 +266,16 @@ public final class MockCentral: CentralManager { keys: lock.keys, newKeys: lock.newKeys ) - await storage.updateState { - $0.notifications[.lockEventsNotifications(UInt8(index))] = KeyListNotification - .from(list: keysList) - .reduce([], { try! $0 + KeysCharacteristic.from($1, id: key, key: keyData, maximumUpdateValueLength: 20) }) - .map { $0.data } + let notifications = KeyListNotification + .from(list: keysList) + .reduce([], { try! $0 + KeysCharacteristic.from($1, id: key, key: keyData, maximumUpdateValueLength: 20) }) + .map { $0.data } + guard let continuation = await storage.state.notifications[.lockKeysNotifications(UInt8(index))] else { + assertionFailure() + return + } + notifications.forEach { + continuation.yield($0) } default: return @@ -331,12 +341,16 @@ public final class MockCentral: CentralManager { guard await storage.state.connected.contains(characteristic.peripheral) else { throw CentralError.disconnected } - return AsyncCentralNotifications { [unowned self] continuation in - try await Task.sleep(nanoseconds: 100_000_000) - if let notifications = await storage.state.notifications[characteristic] { - for notification in notifications { - try await Task.sleep(nanoseconds: 100_000) - continuation(notification) + return AsyncCentralNotifications(bufferSize: 1000, onTermination: { + Task { + await self.storage.updateState { + $0.notifications[characteristic] = nil + } + } + }) { continuation in + Task { + await self.storage.updateState { + $0.notifications[characteristic] = continuation } } } @@ -435,25 +449,7 @@ internal extension MockCentral { .clientCharacteristicConfiguration(.beacon): Data([0x00]), .clientCharacteristicConfiguration(.smartThermostat): Data([0x00]), ] - var notifications: [MockCharacteristic: [Data]] = [ - .batteryLevel: [ - Data([99]), - Data([98]), - Data([95]), - Data([80]), - Data([75]), - Data([25]), - Data([20]), - Data([5]), - Data([1]), - ], - .savantTest: [ - Data(UUID().uuidString.utf8), - Data(UUID().uuidString.utf8), - Data(UUID().uuidString.utf8), - Data(UUID().uuidString.utf8), - ], - ] + var notifications = [MockCharacteristic: AsyncIndefiniteStream.Continuation]() } struct Continuation { From 3b8aaf334a5e5da280f65bc5ebdcb5b793c3a0e1 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 25 Sep 2022 00:05:03 -0700 Subject: [PATCH 163/229] [App] Updated mocked data --- .../AppEntity/LockEventEntity.swift | 2 +- .../AppIntent/FetchEventsIntent.swift | 2 +- Xcode/LockKit/Model/Mock/MockAttributes.swift | 19 ++++++++++++++----- Xcode/LockKit/Model/Mock/MockCentral.swift | 2 ++ Xcode/LockKit/Model/Mock/MockScanData.swift | 9 ++++++++- Xcode/LockKit/Model/NewKeyDocument.swift | 1 - Xcode/LockKit/View/LockDetailView.swift | 13 ++++++++----- 7 files changed, 34 insertions(+), 14 deletions(-) diff --git a/Xcode/LockIntents/AppEntity/LockEventEntity.swift b/Xcode/LockIntents/AppEntity/LockEventEntity.swift index e9abfa39..db4d85f2 100644 --- a/Xcode/LockIntents/AppEntity/LockEventEntity.swift +++ b/Xcode/LockIntents/AppEntity/LockEventEntity.swift @@ -68,7 +68,7 @@ private extension LockEventEntity { var defaultDisplayRepresentation: DisplayRepresentation { DisplayRepresentation( title: "\(String(type.symbol)) \(type.localizedStringResource)", // - \(lock.name ?? "Lock")", - subtitle: "\(date.formatted(date: .abbreviated, time: .shortened))", + subtitle: "\(date.formatted(date: .numeric, time: .shortened))", image: image ) } diff --git a/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift b/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift index a90dd5f3..c0c31712 100644 --- a/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift +++ b/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift @@ -141,7 +141,7 @@ private extension FetchEventsIntent { title: action, subtitle: keyName, trailing: ( - managedObject.date?.formatted(date: .abbreviated, time: .omitted) ?? "", + managedObject.date?.formatted(date: .numeric, time: .omitted) ?? "", managedObject.date?.formatted(date: .omitted, time: .shortened) ?? "" ) ) diff --git a/Xcode/LockKit/Model/Mock/MockAttributes.swift b/Xcode/LockKit/Model/Mock/MockAttributes.swift index 23f7ccc3..9e21b14d 100644 --- a/Xcode/LockKit/Model/Mock/MockAttributes.swift +++ b/Xcode/LockKit/Model/Mock/MockAttributes.swift @@ -110,13 +110,22 @@ extension MockCharacteristic { id: 41, uuid: LockInformationCharacteristic.uuid, peripheral: .lock(id), - properties: [.read] + properties: LockInformationCharacteristic.properties ) } - static func lockEventsRequest(_ id: UInt8) -> MockCharacteristic { + static func unlock(_ id: UInt8) -> MockCharacteristic { Characteristic( id: 42, + uuid: UnlockCharacteristic.uuid, + peripheral: .lock(id), + properties: UnlockCharacteristic.properties + ) + } + + static func lockEventsRequest(_ id: UInt8) -> MockCharacteristic { + Characteristic( + id: 43, uuid: ListEventsCharacteristic.uuid, peripheral: .lock(id), properties: ListEventsCharacteristic.properties @@ -125,7 +134,7 @@ extension MockCharacteristic { static func lockEventsNotifications(_ id: UInt8) -> MockCharacteristic { Characteristic( - id: 43, + id: 44, uuid: EventsCharacteristic.uuid, peripheral: .lock(id), properties: EventsCharacteristic.properties @@ -134,7 +143,7 @@ extension MockCharacteristic { static func lockKeysRequest(_ id: UInt8) -> MockCharacteristic { Characteristic( - id: 44, + id: 45, uuid: ListKeysCharacteristic.uuid, peripheral: .lock(id), properties: ListKeysCharacteristic.properties @@ -143,7 +152,7 @@ extension MockCharacteristic { static func lockKeysNotifications(_ id: UInt8) -> MockCharacteristic { Characteristic( - id: 45, + id: 46, uuid: KeysCharacteristic.uuid, peripheral: .lock(id), properties: KeysCharacteristic.properties diff --git a/Xcode/LockKit/Model/Mock/MockCentral.swift b/Xcode/LockKit/Model/Mock/MockCentral.swift index cc1637b6..9a546d3a 100644 --- a/Xcode/LockKit/Model/Mock/MockCentral.swift +++ b/Xcode/LockKit/Model/Mock/MockCentral.swift @@ -417,6 +417,7 @@ internal extension MockCentral { let id = UInt8(index) return (MockService.lock(id), [ MockCharacteristic.lockInformation(id), + MockCharacteristic.unlock(id), MockCharacteristic.lockEventsRequest(id), MockCharacteristic.lockEventsNotifications(id), MockCharacteristic.lockKeysRequest(id), @@ -439,6 +440,7 @@ internal extension MockCentral { status: lock.status, unlockActions: [.default] ).data + values[.unlock(UInt8(index))] = Data() values[.lockEventsRequest(UInt8(index))] = Data() values[.lockKeysRequest(UInt8(index))] = Data() } diff --git a/Xcode/LockKit/Model/Mock/MockScanData.swift b/Xcode/LockKit/Model/Mock/MockScanData.swift index 87bf4266..f2cf9b03 100644 --- a/Xcode/LockKit/Model/Mock/MockScanData.swift +++ b/Xcode/LockKit/Model/Mock/MockScanData.swift @@ -49,7 +49,14 @@ public extension MockScanData { public extension MockCentral.Peripheral { static var random: MockCentral.Peripheral { - Peripheral(id: BluetoothAddress(bytes: (.random(in: .min ... .max), .random(in: .min ... .max), .random(in: .min ... .max), .random(in: .min ... .max), .random(in: .min ... .max), .random(in: .min ... .max)))) + Peripheral(id: BluetoothAddress(bytes: ( + .random(in: .min ... .max), + .random(in: .min ... .max), + .random(in: .min ... .max), + .random(in: .min ... .max), + .random(in: .min ... .max), + .random(in: .min ... .max))) + ) } static var beacon: Peripheral { diff --git a/Xcode/LockKit/Model/NewKeyDocument.swift b/Xcode/LockKit/Model/NewKeyDocument.swift index 62621994..270fb098 100644 --- a/Xcode/LockKit/Model/NewKeyDocument.swift +++ b/Xcode/LockKit/Model/NewKeyDocument.swift @@ -29,7 +29,6 @@ public extension NewKey.Invitation { } public func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { - fatalError() } } diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 33070603..289a972e 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -70,13 +70,17 @@ public struct LockDetailView: View { #endif ToolbarItem(placement: .primaryAction) { - Button(action: { }) { + Button(action: { + + }) { Image(systemSymbol: .pencil) } } ToolbarItem(placement: .primaryAction) { - Button(action: { }) { + Button(action: { + + }) { Image(systemSymbol: .trash) } } @@ -96,7 +100,7 @@ public struct LockDetailView: View { #if os(iOS) AnyView(SetupLockView(id: id)) #else - AnyView(Text("Scan to Setup")) + AnyView(Text("Setup this lock on your iOS device.")) #endif } else { AnyView(UnknownView(id: id, information: information)) @@ -201,7 +205,6 @@ private extension LockDetailView { } func unlock() { - // FIXME: Handle errors Task { guard await store.central.state == .poweredOn else { return @@ -211,7 +214,7 @@ private extension LockDetailView { let policy: LAPolicy = .deviceOwnerAuthenticationWithBiometrics let reason = NSLocalizedString("Biometrics are needed to unlock", comment: "") do { - if try authentication.canEvaluate(policy: policy) { + if (try? authentication.canEvaluate(policy: policy)) ?? false { try await authentication.evaluatePolicy(policy, localizedReason: reason) } } From ae108cb0624decaaa6b0399511d64215e1ea712f Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 25 Sep 2022 00:28:57 -0700 Subject: [PATCH 164/229] [App] Fixed `AppNavigationLink` --- Xcode/LockKit/Model/Mock/MockLock.swift | 4 ++-- Xcode/LockKit/View/NavigationLink.swift | 9 +++++---- Xcode/SmartLock/View/TabBarView.swift | 22 ++++++++++++++++++---- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/Xcode/LockKit/Model/Mock/MockLock.swift b/Xcode/LockKit/Model/Mock/MockLock.swift index 46b095b2..9e66b5f8 100644 --- a/Xcode/LockKit/Model/Mock/MockLock.swift +++ b/Xcode/LockKit/Model/Mock/MockLock.swift @@ -94,7 +94,7 @@ public extension MockLock { .setup( .init( id: UUID(uuidString: "3CEAD223-2CBF-4216-85CD-CAD79302E201")!, - date: Date() - TimeInterval(60 * (3 - 0 + 1)), + date: Date() - TimeInterval(60 * (3 - 1 + 1)), key: UUID(uuidString: "53F21D45-2E82-43CC-9FDC-18313511\(UInt16(0x01).toHexadecimal())")! ) ), @@ -124,7 +124,7 @@ public extension MockLock { .setup( .init( id: UUID(uuidString: "3CEAD223-2CBF-4216-85CD-CAD79302E202")!, - date: Date() - TimeInterval(60 * (3 - 0 + 1)), + date: Date() - TimeInterval(60 * (3 - 2 + 1)), key: UUID(uuidString: "53F21D45-2E82-43CC-9FDC-18313511\(UInt16(0x02).toHexadecimal())")! ) ), diff --git a/Xcode/LockKit/View/NavigationLink.swift b/Xcode/LockKit/View/NavigationLink.swift index fea5bac0..7b45f689 100644 --- a/Xcode/LockKit/View/NavigationLink.swift +++ b/Xcode/LockKit/View/NavigationLink.swift @@ -17,11 +17,15 @@ public struct AppNavigationLink : View { private let label: Label public var body: some View { + #if os(macOS) if #available(macOS 13, iOS 16, tvOS 16, *) { navigationStackLink } else { - navigationViewLink + } + #else + navigationViewLink + #endif } public init(id: ID, label: () -> Label) { @@ -43,9 +47,6 @@ private extension AppNavigationLink { @available(macOS 13, iOS 16, tvOS 16, *) var navigationStackLink: some View { NavigationLink(value: id, label: { label }) - .navigationDestination(for: AppNavigationLinkID.self) { - AppNavigationDestinationView(id: $0) - } } } diff --git a/Xcode/SmartLock/View/TabBarView.swift b/Xcode/SmartLock/View/TabBarView.swift index ce2a243c..e1733bbe 100644 --- a/Xcode/SmartLock/View/TabBarView.swift +++ b/Xcode/SmartLock/View/TabBarView.swift @@ -84,14 +84,28 @@ extension TabBarView { private extension TabBarView.SplitView { var navigationView: some View { + if #available(iOS 16.0, *) { + return NavigationView { + sidebar() + } + .navigationViewStyle(.stack) + } else { + return NavigationView { + sidebar() + } + .navigationViewStyle(.stack) + } + } + + var navigationStack: some View { if #available(iOS 16.0, *) { return NavigationSplitView( columnVisibility: columnVisibility, - sidebar: sidebar, + sidebar: { + sidebar() + }, detail: { - detail().navigationDestination(for: AppNavigationLinkID.self) { - AppNavigationDestinationView(id: $0) - } + detail() } ) .navigationSplitViewStyle(.prominentDetail) From 1a906c8c018cbeb421f53887220443213165d6b5 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 25 Sep 2022 02:03:13 -0700 Subject: [PATCH 165/229] [App] Added `FetchKeysIntent` --- Xcode/LockIntents/AppEntity/KeyEntity.swift | 30 ++++- Xcode/LockIntents/AppEntity/LockEntity.swift | 12 +- .../AppEntity/LockEventEntity.swift | 7 +- .../AppIntent/FetchEventsIntent.swift | 2 +- .../AppIntent/FetchKeysIntent.swift | 115 ++++++++++++++++++ Xcode/LockIntents/EntityQuery/KeyQuery.swift | 8 +- Xcode/LockKit/Model/Store.swift | 27 ++-- Xcode/SmartLock.xcodeproj/project.pbxproj | 4 + 8 files changed, 177 insertions(+), 28 deletions(-) create mode 100644 Xcode/LockIntents/AppIntent/FetchKeysIntent.swift diff --git a/Xcode/LockIntents/AppEntity/KeyEntity.swift b/Xcode/LockIntents/AppEntity/KeyEntity.swift index cfd7b3ec..a90efc4b 100644 --- a/Xcode/LockIntents/AppEntity/KeyEntity.swift +++ b/Xcode/LockIntents/AppEntity/KeyEntity.swift @@ -12,19 +12,23 @@ import LockKit struct KeyEntity: AppEntity, Identifiable { /// The unique identifier of the key. - var id: UUID + let id: UUID /// Lock associated with this key. - var lock: UUID + let lock: UUID /// The name of the key. - var name: String + let name: String /// Date key was created. - var created: Date + let created: Date /// Key's permissions. - var permission: PermissionAppEnum + let permission: PermissionAppEnum + + let isPending: Bool + + let expiration: Date? } @available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) @@ -36,9 +40,11 @@ extension KeyEntity { "Key" } + @MainActor var displayRepresentation: DisplayRepresentation { + let lock = Store.shared.applicationData.locks[lock]?.name return DisplayRepresentation( - title: "\(name)", + title: lock.map { "\($0) - \(name)" } ?? "\(name)", subtitle: "\(permission.localizedStringResource)", image: .init(named: permission.imageName, isTemplate: false) ) @@ -54,5 +60,17 @@ extension KeyEntity { self.name = key.name self.created = key.created self.permission = .init(rawValue: key.permission.type.rawValue)! + self.isPending = false + self.expiration = nil + } + + init(newKey: NewKey, lock: UUID) { + self.id = newKey.id + self.lock = lock + self.name = newKey.name + self.created = newKey.created + self.permission = .init(rawValue: newKey.permission.type.rawValue)! + self.isPending = true + self.expiration = newKey.expiration } } diff --git a/Xcode/LockIntents/AppEntity/LockEntity.swift b/Xcode/LockIntents/AppEntity/LockEntity.swift index afaf991b..629a1ab6 100644 --- a/Xcode/LockIntents/AppEntity/LockEntity.swift +++ b/Xcode/LockIntents/AppEntity/LockEntity.swift @@ -15,22 +15,22 @@ struct LockEntity: AppEntity, Identifiable { let id: UUID /// Firmware build number - var buildVersion: UInt64 + let buildVersion: UInt64 /// Firmware version - var version: String + let version: String /// Device state - var status: LockStatusAppEnum + let status: LockStatusAppEnum /// Supported lock actions - var unlockActions: Set + let unlockActions: Set /// Stored name - var name: String? + let name: String? /// Associated key - var key: KeyEntity? + let key: KeyEntity? } @available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) diff --git a/Xcode/LockIntents/AppEntity/LockEventEntity.swift b/Xcode/LockIntents/AppEntity/LockEventEntity.swift index db4d85f2..29a934e3 100644 --- a/Xcode/LockIntents/AppEntity/LockEventEntity.swift +++ b/Xcode/LockIntents/AppEntity/LockEventEntity.swift @@ -23,6 +23,8 @@ struct LockEventEntity: AppEntity, Identifiable { let id: UUID + let lock: UUID + /// Date event was created let date: Date @@ -99,12 +101,14 @@ extension LockEventEntity { init?(managedObject: EventManagedObject) { guard let id = managedObject.identifier, + let lock = managedObject.lock?.identifier, let date = managedObject.date, let key = managedObject.key, let eventType = EventTypeAppEnum(rawValue: Swift.type(of: managedObject).eventType.rawValue) else { return nil } self.id = id + self.lock = lock self.date = date self.key = key self.type = eventType @@ -114,9 +118,10 @@ extension LockEventEntity { @available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) extension LockEventEntity { - init(_ value: LockEvent) { + init(_ value: LockEvent, lock: UUID) { self.id = value.id + self.lock = lock self.date = value.date self.key = value.key self.type = .init(rawValue: value.type.rawValue)! diff --git a/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift b/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift index c0c31712..c34272c5 100644 --- a/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift +++ b/Xcode/LockIntents/AppIntent/FetchEventsIntent.swift @@ -86,7 +86,7 @@ struct FetchEventsIntent: AppIntent { let managedObjects = try events.compactMap { try EventManagedObject.find($0.id, in: managedObjectContext) } assert(managedObjects.count == events.count) return .result( - value: events.map { LockEventEntity($0) }, + value: events.map { LockEventEntity($0, lock: lock.id) }, view: view(for: managedObjects, in: managedObjectContext) ) } diff --git a/Xcode/LockIntents/AppIntent/FetchKeysIntent.swift b/Xcode/LockIntents/AppIntent/FetchKeysIntent.swift new file mode 100644 index 00000000..59a4e4de --- /dev/null +++ b/Xcode/LockIntents/AppIntent/FetchKeysIntent.swift @@ -0,0 +1,115 @@ +// +// FetchKeysIntent.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/25/22. +// + +import Foundation +import AppIntents +import CoreData +import SwiftUI +import LockKit + +/// Intent for fetching events +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +struct FetchKeysIntent: AppIntent { + + static var title: LocalizedStringResource { "Fetch Keys" } + + static var description: IntentDescription { + IntentDescription( + "Fetch the keys for a specified lock.", + categoryName: "Utility", + searchKeywords: ["events", "bluetooth", "lock"] + ) + } + + /// The specified lock to unlock. + @Parameter( + title: "Lock", + description: "The specified lock to fetch keys from." + ) + var lock: LockEntity + + @MainActor + func perform() async throws -> some IntentResult { + let store = Store.shared + // search for lock if not in cache + guard let peripheral = try await store.device(for: lock.id) else { + throw LockError.notInRange(lock: lock.id) + } + // fetch events + let keysList = try await store.listKeys(for: peripheral) + let keys = keysList.keys.map { KeyEntity(key: $0, lock: lock.id) } + + keysList.newKeys.map { KeyEntity(newKey: $0, lock: lock.id) } + return .result( + value: keys, + view: view(for: keysList) + ) + } +} + +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +@MainActor +private extension FetchKeysIntent { + + static let relativeDateTimeFormatter: RelativeDateTimeFormatter = { + let dateFormatter = RelativeDateTimeFormatter() + dateFormatter.dateTimeStyle = .numeric + dateFormatter.unitsStyle = .spellOut + return dateFormatter + }() + + func view(for list: KeysList) -> some View { + HStack(alignment: .top, spacing: 0) { + VStack(alignment: .leading, spacing: 8) { + if list.isEmpty { + Text("No keys found.") + .padding(20) + } else { + if list.count > 4 { + Text("Found \(list.count) keys.") + .padding(20) + } else { + ForEach(list.keys) { + view(for: $0) + } + if list.newKeys.isEmpty == false { + Section("Pending") { + ForEach(list.newKeys) { + view(for: $0) + } + } + } + } + } + } + Spacer(minLength: 0) + } + } + + func view(for key: Key) -> some View { + LockRowView( + image: .image(Image(permissionType: key.permission.type)), + title: key.name, + subtitle: key.permission.localizedText, + trailing: ( + key.created.formatted(date: .numeric, time: .omitted), + key.created.formatted(date: .omitted, time: .shortened) + ) + ) + } + + func view(for key: NewKey) -> some View { + LockRowView( + image: .image(Image(permissionType: key.permission.type)), + title: key.name, + subtitle: key.permission.localizedText + "\n" + "Expires " + FetchKeysIntent.relativeDateTimeFormatter.localizedString(for: key.expiration, relativeTo: Date()), + trailing: ( + key.created.formatted(date: .numeric, time: .omitted), + key.created.formatted(date: .omitted, time: .shortened) + ) + ) + } +} diff --git a/Xcode/LockIntents/EntityQuery/KeyQuery.swift b/Xcode/LockIntents/EntityQuery/KeyQuery.swift index 2e27610b..91de3bd6 100644 --- a/Xcode/LockIntents/EntityQuery/KeyQuery.swift +++ b/Xcode/LockIntents/EntityQuery/KeyQuery.swift @@ -21,7 +21,9 @@ struct KeyQuery: EntityQuery { lock: $0.key, name: $0.value.key.name, created: $0.value.key.created, - permission: .init(rawValue: $0.value.key.permission.type.rawValue)! + permission: .init(rawValue: $0.value.key.permission.type.rawValue)!, + isPending: false, + expiration: nil ) } } @@ -35,7 +37,9 @@ struct KeyQuery: EntityQuery { lock: $0.key, name: $0.value.key.name, created: $0.value.key.created, - permission: .init(rawValue: $0.value.key.permission.type.rawValue)! + permission: .init(rawValue: $0.value.key.permission.type.rawValue)!, + isPending: false, + expiration: nil ) } } diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 2269eec6..1bdc9bff 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -615,18 +615,20 @@ public extension Store { log("Confirmed new key for lock \(information.id)") } + @discardableResult func listKeys( for peripheral: NativeCentral.Peripheral - ) async throws { + ) async throws -> KeysList { stopScanning() - try await central.connection(for: peripheral) { + return try await central.connection(for: peripheral) { try await self.listKeys(for: $0) } } + @discardableResult func listKeys( for connection: GATTConnection - ) async throws { + ) async throws -> KeysList { let peripheral = connection.peripheral // get lock key guard let information = self.lockInformation[peripheral] else { @@ -643,20 +645,19 @@ public extension Store { ) let context = backgroundContext - var keys = Set() - var newKeys = Set() + var keysList = KeysList() // BLE request let centralLog = central.log let stream = try await connection.listKeys(using: key, log: centralLog) for try await notification in stream { switch notification.key { case let .key(key): - keys.insert(key.id) centralLog?("Recieved \(key.permission.type) key \(key.id) \(key.name)") case let .newKey(key): - newKeys.insert(key.id) centralLog?("Recieved \(key.permission.type) pending key \(key.id) \(key.name)") } + // + keysList.append(notification.key) // insert key to CoreData await context.commit { (context) in try context.insert(notification.key, for: information.id) @@ -673,7 +674,7 @@ public extension Store { && .compound(.not( .comparison( Comparison( - left: .value(.collection(keys.map { .uuid($0) })), + left: .value(.collection(keysList.keys.map { .uuid($0.id) })), right: .keyPath(#keyPath(KeyManagedObject.identifier)), type: .contains ) @@ -685,7 +686,7 @@ public extension Store { format: "%K == %@ && NOT %@ CONTAINS %K", #keyPath(KeyManagedObject.lock.identifier), lockIdentifier as NSUUID, - keys as NSSet, + Set(keysList.keys.map({ $0.id })) as NSSet, #keyPath(KeyManagedObject.identifier) ).description == predicate.description) assert(predicate.description == predicate.toFoundation().description) @@ -707,7 +708,7 @@ public extension Store { && .compound(.not( .comparison( Comparison( - left: .value(.collection(newKeys.map { .uuid($0) })), + left: .value(.collection(keysList.newKeys.map { .uuid($0.id) })), right: .keyPath(#keyPath(NewKeyManagedObject.identifier)), type: .contains ) @@ -719,7 +720,7 @@ public extension Store { format: "%K == %@ && NOT %@ CONTAINS %K", #keyPath(NewKeyManagedObject.lock.identifier), lockIdentifier as NSUUID, - newKeys as NSSet, + Set(keysList.newKeys.map { $0.id }) as NSSet, #keyPath(NewKeyManagedObject.identifier) ).description == predicate.description) // fetch @@ -748,7 +749,9 @@ public extension Store { //updateCloud() } - log("Recieved \(keys.count) keys and \(newKeys.count) pending keys for lock \(information.id)") + log("Recieved \(keysList.keys.count) keys and \(keysList.newKeys.count) pending keys for lock \(information.id)") + + return keysList } @discardableResult diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 7a497351..99d557a9 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 6E10C90428DFD18D00703691 /* EventTypeAppEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C90328DFD18D00703691 /* EventTypeAppEnum.swift */; }; 6E10C90628DFD26300703691 /* LockEventQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C90528DFD26300703691 /* LockEventQuery.swift */; }; 6E10C90828E0006700703691 /* Hexadecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C90728E0006700703691 /* Hexadecimal.swift */; }; + 6E10C90A28E03ED700703691 /* FetchKeysIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C90928E03ED700703691 /* FetchKeysIntent.swift */; }; 6E169A7828D9A34F008545EC /* NavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7728D9A34F008545EC /* NavigationLink.swift */; }; 6E169A7D28D9C097008545EC /* PermissionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7C28D9C097008545EC /* PermissionsView.swift */; }; 6E169A7F28D9C135008545EC /* NewPermissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7E28D9C135008545EC /* NewPermissionView.swift */; }; @@ -160,6 +161,7 @@ 6E10C90328DFD18D00703691 /* EventTypeAppEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTypeAppEnum.swift; sourceTree = ""; }; 6E10C90528DFD26300703691 /* LockEventQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockEventQuery.swift; sourceTree = ""; }; 6E10C90728E0006700703691 /* Hexadecimal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Hexadecimal.swift; sourceTree = ""; }; + 6E10C90928E03ED700703691 /* FetchKeysIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchKeysIntent.swift; sourceTree = ""; }; 6E169A7728D9A34F008545EC /* NavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationLink.swift; sourceTree = ""; }; 6E169A7C28D9C097008545EC /* PermissionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsView.swift; sourceTree = ""; }; 6E169A7E28D9C135008545EC /* NewPermissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPermissionView.swift; sourceTree = ""; }; @@ -490,6 +492,7 @@ 6E8BBFEC28DD301B00F03735 /* ScanLocksIntent.swift */, 6E8BBFFF28DD492300F03735 /* UnlockIntent.swift */, 6E84E54428DE89BC008CAE85 /* FetchEventsIntent.swift */, + 6E10C90928E03ED700703691 /* FetchKeysIntent.swift */, ); path = AppIntent; sourceTree = ""; @@ -812,6 +815,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6E10C90A28E03ED700703691 /* FetchKeysIntent.swift in Sources */, 6E2182F128D7B4FA00A622B3 /* SettingsView.swift in Sources */, 6E84E52D28DDC841008CAE85 /* LockEntity.swift in Sources */, 6E10C90228DFCFED00703691 /* LockEventEntity.swift in Sources */, From 4fda770f9fa12808bd86c2b3657f4f5897370cec Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 25 Sep 2022 04:23:25 -0700 Subject: [PATCH 166/229] [CoreLock] Updated logging --- Sources/CoreLock/Bluetooth/Notification.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/CoreLock/Bluetooth/Notification.swift b/Sources/CoreLock/Bluetooth/Notification.swift index 08631e50..f8545e15 100644 --- a/Sources/CoreLock/Bluetooth/Notification.swift +++ b/Sources/CoreLock/Bluetooth/Notification.swift @@ -59,19 +59,22 @@ internal extension GATTConnection { return AsyncThrowingStream(ChunkNotification.Notification.self, bufferingPolicy: .unbounded) { continuation in Task.detached { do { + var notificationsCount = 0 var chunks = [Chunk]() chunks.reserveCapacity(2) for try await chunkNotification in stream { let chunk = chunkNotification.chunk - log?("Received chunk \(chunks.count + 1) (\(chunk.bytes.count) bytes)") chunks.append(chunk) + log?("Received chunk \(chunks.count) (\(chunks.length)/\(chunk.total))") assert(chunks.isEmpty == false) guard chunks.length >= chunk.total else { continue // wait for more chunks } let notificationValue = try ChunkNotification.from(chunks: chunks, using: key.secret) - chunks.removeAll(keepingCapacity: true) continuation.yield(notificationValue) + notificationsCount += 1 + log?("Received\(notificationValue.isLast ? " last" : "") notification \(notificationsCount)") + chunks.removeAll(keepingCapacity: true) guard notificationValue.isLast else { continue // wait for final value } From f89350d0e7da6db631c37dea3a25364467ea599e Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 25 Sep 2022 04:24:00 -0700 Subject: [PATCH 167/229] [App] Added `NewKeyIntent` --- .../AppIntent/FetchKeysIntent.swift | 2 +- .../LockIntents/AppIntent/NewKeyIntent.swift | 71 +++++++++++++++++++ Xcode/SmartLock.xcodeproj/project.pbxproj | 4 ++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 Xcode/LockIntents/AppIntent/NewKeyIntent.swift diff --git a/Xcode/LockIntents/AppIntent/FetchKeysIntent.swift b/Xcode/LockIntents/AppIntent/FetchKeysIntent.swift index 59a4e4de..d8c9ad14 100644 --- a/Xcode/LockIntents/AppIntent/FetchKeysIntent.swift +++ b/Xcode/LockIntents/AppIntent/FetchKeysIntent.swift @@ -21,7 +21,7 @@ struct FetchKeysIntent: AppIntent { IntentDescription( "Fetch the keys for a specified lock.", categoryName: "Utility", - searchKeywords: ["events", "bluetooth", "lock"] + searchKeywords: ["key", "bluetooth", "lock"] ) } diff --git a/Xcode/LockIntents/AppIntent/NewKeyIntent.swift b/Xcode/LockIntents/AppIntent/NewKeyIntent.swift new file mode 100644 index 00000000..18f4333c --- /dev/null +++ b/Xcode/LockIntents/AppIntent/NewKeyIntent.swift @@ -0,0 +1,71 @@ +// +// NewKeyIntent.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/25/22. +// + +import Foundation +import AppIntents +import CoreData +import SwiftUI +import LockKit + +/// Intent for fetching events +@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) +struct NewKeyIntent: AppIntent { + + static var title: LocalizedStringResource { "New Key" } + + static var description: IntentDescription { + IntentDescription( + "Create a new key for a specified lock.", + categoryName: "Utility", + searchKeywords: ["key", "bluetooth", "lock"] + ) + } + + /// The specified lock to create a new key. + @Parameter( + title: "Lock", + description: "The specified lock to create a new key." + ) + var lock: LockEntity + + /// The specified lock to create a new key. + @Parameter( + title: "Name", + description: "The name of the new key." + ) + var name: String + + /// The permission of the new key. + @Parameter( + title: "Permission", + description: "The specified permission of the new key." + ) + var permission: PermissionAppEnum + + @MainActor + func perform() async throws -> some IntentResult { + let store = Store.shared + // search for lock if not in cache + guard let peripheral = try await store.device(for: lock.id) else { + throw LockError.notInRange(lock: lock.id) + } + let name = name.isEmpty ? "New \(permission) Key" : self.name + // fetch events + let invitation = try await store.newKey( + for: peripheral, + permission: .anytime, //PermissionType(rawValue: permission.rawValue)!, + name: name + ) + let fileName = "newKey-\(invitation.key.id).ekey" + let encoder = JSONEncoder() + let data = try encoder.encode(invitation) + let file = IntentFile(data: data, filename: fileName) + return .result( + value: file + ) + } +} diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 99d557a9..8841fad0 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 6E10C90628DFD26300703691 /* LockEventQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C90528DFD26300703691 /* LockEventQuery.swift */; }; 6E10C90828E0006700703691 /* Hexadecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C90728E0006700703691 /* Hexadecimal.swift */; }; 6E10C90A28E03ED700703691 /* FetchKeysIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C90928E03ED700703691 /* FetchKeysIntent.swift */; }; + 6E10C90C28E0518F00703691 /* NewKeyIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C90B28E0518F00703691 /* NewKeyIntent.swift */; }; 6E169A7828D9A34F008545EC /* NavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7728D9A34F008545EC /* NavigationLink.swift */; }; 6E169A7D28D9C097008545EC /* PermissionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7C28D9C097008545EC /* PermissionsView.swift */; }; 6E169A7F28D9C135008545EC /* NewPermissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7E28D9C135008545EC /* NewPermissionView.swift */; }; @@ -162,6 +163,7 @@ 6E10C90528DFD26300703691 /* LockEventQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockEventQuery.swift; sourceTree = ""; }; 6E10C90728E0006700703691 /* Hexadecimal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Hexadecimal.swift; sourceTree = ""; }; 6E10C90928E03ED700703691 /* FetchKeysIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchKeysIntent.swift; sourceTree = ""; }; + 6E10C90B28E0518F00703691 /* NewKeyIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyIntent.swift; sourceTree = ""; }; 6E169A7728D9A34F008545EC /* NavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationLink.swift; sourceTree = ""; }; 6E169A7C28D9C097008545EC /* PermissionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsView.swift; sourceTree = ""; }; 6E169A7E28D9C135008545EC /* NewPermissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPermissionView.swift; sourceTree = ""; }; @@ -493,6 +495,7 @@ 6E8BBFFF28DD492300F03735 /* UnlockIntent.swift */, 6E84E54428DE89BC008CAE85 /* FetchEventsIntent.swift */, 6E10C90928E03ED700703691 /* FetchKeysIntent.swift */, + 6E10C90B28E0518F00703691 /* NewKeyIntent.swift */, ); path = AppIntent; sourceTree = ""; @@ -821,6 +824,7 @@ 6E10C90228DFCFED00703691 /* LockEventEntity.swift in Sources */, 6E10C90428DFD18D00703691 /* EventTypeAppEnum.swift in Sources */, 6E84E52728DDC841008CAE85 /* ScanLocksIntent.swift in Sources */, + 6E10C90C28E0518F00703691 /* NewKeyIntent.swift in Sources */, 6E84E54628DEA0CE008CAE85 /* FetchEventsIntent.swift in Sources */, 6E84E52A28DDC841008CAE85 /* Shortcuts.swift in Sources */, 6E84E54328DE878D008CAE85 /* PermissionAppEnum.swift in Sources */, From ac864e98b3f32f543578307b1f690ad1f811cc2a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 25 Sep 2022 04:24:47 -0700 Subject: [PATCH 168/229] [App] Fixed NSPredicate usage --- Xcode/LockKit/Model/Store.swift | 39 +++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index 1bdc9bff..bbf01244 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -686,7 +686,7 @@ public extension Store { format: "%K == %@ && NOT %@ CONTAINS %K", #keyPath(KeyManagedObject.lock.identifier), lockIdentifier as NSUUID, - Set(keysList.keys.map({ $0.id })) as NSSet, + keysList.keys.map({ $0.id }) as NSArray, #keyPath(KeyManagedObject.identifier) ).description == predicate.description) assert(predicate.description == predicate.toFoundation().description) @@ -720,7 +720,7 @@ public extension Store { format: "%K == %@ && NOT %@ CONTAINS %K", #keyPath(NewKeyManagedObject.lock.identifier), lockIdentifier as NSUUID, - Set(keysList.newKeys.map { $0.id }) as NSSet, + keysList.newKeys.map { $0.id } as NSArray, #keyPath(NewKeyManagedObject.identifier) ).description == predicate.description) // fetch @@ -790,22 +790,27 @@ public extension Store { events.reserveCapacity(Int(fetchRequest?.limit ?? 10)) // BLE request let centralLog = central.log - let stream = try await connection.listEvents(fetchRequest: fetchRequest, using: key, log: centralLog) + let stream = try await connection.listEvents( + fetchRequest: fetchRequest, + using: key, + log: centralLog + ) for try await notification in stream { - if let event = notification.event { - centralLog?("Recieved \(event.type) event \(event.id)") - events.append(event) - // store in CoreData - await context.commit { (context) in - try context.insert(event, for: information.id) - } - // upload to iCloud - if preferences.isCloudBackupEnabled { - // perform concurrently - Task { - let value = LockEvent.Cloud(event: event, for: lockIdentifier) - try await self.cloud.upload(value) - } + guard let event = notification.event else { + break + } + centralLog?("Recieved \(event.type) event \(event.id)") + events.append(event) + // store in CoreData + await context.commit { (context) in + try context.insert(event, for: information.id) + } + // upload to iCloud + if preferences.isCloudBackupEnabled { + // perform concurrently + Task { + let value = LockEvent.Cloud(event: event, for: lockIdentifier) + try await self.cloud.upload(value) } } } From b7ba53660e60d8e1ec2199e79374884199b87d23 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 25 Sep 2022 17:44:29 -0700 Subject: [PATCH 169/229] [App] Added `NewKeyInvitationStore.DataSource` --- Xcode/LockKit/Model/AsyncFetchRequest.swift | 29 ++- .../LockKit/Model/NewKeyInvitationStore.swift | 101 +++++----- .../Model/NewKeysAsyncFetchRequest.swift | 51 +++++ Xcode/LockKit/Model/Store.swift | 8 +- Xcode/LockKit/View/LockDetailView.swift | 8 +- Xcode/LockKit/View/LockRowView.swift | 2 +- Xcode/LockKit/View/NavigationLink.swift | 2 +- Xcode/LockKit/View/NewPermissionView.swift | 13 +- Xcode/SmartLock.xcodeproj/project.pbxproj | 4 + Xcode/SmartLock/View/KeysView.swift | 187 ++++++++++++++++-- 10 files changed, 314 insertions(+), 91 deletions(-) create mode 100644 Xcode/LockKit/Model/NewKeysAsyncFetchRequest.swift diff --git a/Xcode/LockKit/Model/AsyncFetchRequest.swift b/Xcode/LockKit/Model/AsyncFetchRequest.swift index 6297154a..6b8d3674 100644 --- a/Xcode/LockKit/Model/AsyncFetchRequest.swift +++ b/Xcode/LockKit/Model/AsyncFetchRequest.swift @@ -24,7 +24,6 @@ public struct AsyncFetchRequest { self.configuration = configuration } - @MainActor public var wrappedValue: AsyncFetchedResults { return .init( dataSource: dataSource, @@ -33,7 +32,19 @@ public struct AsyncFetchRequest { } } -public protocol AsyncFetchDataSource: AnyObject, ObservableObject { +public extension AsyncFetchRequest where DataSource.Configuration == Void { + + init( + dataSource: DataSource + ) { + self.init( + dataSource: dataSource, + configuration: () + ) + } +} + +public protocol AsyncFetchDataSource { associatedtype ID: Hashable @@ -50,18 +61,20 @@ public protocol AsyncFetchDataSource: AnyObject, ObservableObject { func fetch(configuration: Configuration) -> [ID] /// Asyncronously load the specified item. - func load(id: ID) async -> Result + func load(_ id: ID) async -> Result } extension AsyncFetchRequest: DynamicProperty { - //public mutating func update() { + public mutating func update() { + print(#function) + } } @MainActor public struct AsyncFetchedResults { - @ObservedObject + //@ObservedObject internal var dataSource: DataSource internal var configuration: DataSource.Configuration @@ -148,6 +161,10 @@ extension AsyncFetchedResults: RandomAccessCollection { return results.count } + public var isEmpty: Bool { + count == 0 + } + public var startIndex: Int { results.startIndex } @@ -170,7 +187,7 @@ extension AsyncFetchedResults: RandomAccessCollection { if tasks[id] == nil { tasks[id] = Task { // load - let result = await dataSource.load(id: id) + let result = await dataSource.load(id) // save error switch result { case .success: diff --git a/Xcode/LockKit/Model/NewKeyInvitationStore.swift b/Xcode/LockKit/Model/NewKeyInvitationStore.swift index 2c13e721..01727a09 100644 --- a/Xcode/LockKit/Model/NewKeyInvitationStore.swift +++ b/Xcode/LockKit/Model/NewKeyInvitationStore.swift @@ -13,15 +13,51 @@ public final class NewKeyInvitationStore: ObservableObject { public typealias Cache = [URL: NewKey.Invitation] + // MARK: - Properties + @Published public private(set) var cache = Cache() - internal lazy var fileManager = FileManager() + internal let fileManager = FileManager() + + internal let encoder = JSONEncoder() + + internal let decoder = JSONDecoder() + + // MARK: - Initialization public static let shared = NewKeyInvitationStore() private init() { } + // MARK: - Methods + + public func fetchDocuments() throws -> [URL] { + return try fileManager + .contentsOfDirectory( + at: documentsURL, + includingPropertiesForKeys: [ + .creationDateKey + ], + options: [.skipsHiddenFiles] + ) + .lazy + .filter { $0.lastPathComponent.hasSuffix(".ekey") } + .map { (url: $0, date: (try? self.fileManager.attributesOfItem(atPath: $0.path)[FileAttributeKey.creationDate] as? Date) ?? Date()) } + .sorted { $0.date < $1.date } + .map { $0.url } + } + + @discardableResult + public func delete(_ url: URL) async throws -> Bool { + guard fileManager.fileExists(atPath: url.path) else { + return false + } + try fileManager.removeItem(at: url) + await removeCached(url) + return true + } + @discardableResult public func save( _ invitation: NewKey.Invitation, @@ -30,7 +66,6 @@ public final class NewKeyInvitationStore: ObservableObject { let documentsURL = try self.documentsURL let fileName = fileName ?? "newKey-\(invitation.key.id).ekey" let fileURL = documentsURL.appendingPathComponent(fileName) - let encoder = JSONEncoder() let writeTask = Task { let data = try encoder.encode(invitation) try data.write(to: fileURL, options: [.atomic]) @@ -40,55 +75,14 @@ public final class NewKeyInvitationStore: ObservableObject { await self.cache(invitation, url: fileURL) return fileURL } - /* - @discardableResult - public func fetchAll() async throws -> Cache { - let documentsURL = try self.documentsURL - let files = try fileManager.contentsOfDirectory(atPath: documentsURL.path) - let keyFiles = files.filter { $0.hasSuffix(".ekey") } - - // attempt to read concurrently - let oldValue = await MainActor.run { self.cache } - let newValue = await withTaskGroup(of: (URL, Result).self, returning: Cache.self) { taskGroup in - for path in keyFiles { - let url = URL(fileURLWithPath: path) - taskGroup.addTask { - do { - let decoder = JSONDecoder() - let data = try Data(contentsOf: url, options: [.mappedIfSafe]) - let value = try decoder.decode(NewKey.Invitation.self, from: data) - // update UI incrementally - await self.cache(value, url: url) - return (url, .success(value)) - } - catch { - log("⚠️ Unable to read \(url.lastPathComponent). \(error.localizedDescription)") - return (url, .failure(error)) - } - } - } - - // build result serially - var newValue = Cache() - newValue.reserveCapacity(oldValue.count + 2) - for await value in taskGroup { - switch value { - case let .success(newKey): - newValue[newKey] - case let .failure(error): - // decrement count - break - } - } - return newValue - } - // replace everything - await MainActor.run { - self.cache = newValue - } - return newValue + + public func load(_ url: URL) async throws -> NewKey.Invitation { + let data = try Data(contentsOf: url, options: [.mappedIfSafe]) + let invitation = try decoder.decode(NewKey.Invitation.self, from: data) + await cache(invitation, url: url) + return invitation } - */ + internal var documentsURL: URL { get throws { guard let url = fileManager.documentsURL else { @@ -99,7 +93,12 @@ public final class NewKeyInvitationStore: ObservableObject { } @MainActor - private func cache(_ value: NewKey.Invitation, url: URL) async { + private func cache(_ value: NewKey.Invitation, url: URL) { self.cache[url] = value } + + @MainActor + private func removeCached(_ url: URL) { + self.cache.removeValue(forKey: url) + } } diff --git a/Xcode/LockKit/Model/NewKeysAsyncFetchRequest.swift b/Xcode/LockKit/Model/NewKeysAsyncFetchRequest.swift new file mode 100644 index 00000000..b0edda45 --- /dev/null +++ b/Xcode/LockKit/Model/NewKeysAsyncFetchRequest.swift @@ -0,0 +1,51 @@ +// +// NewKeysAsyncFetchRequest.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/25/22. +// + +import Foundation +import CoreLock + +public extension NewKeyInvitationStore { + + struct DataSource: AsyncFetchDataSource { + + public typealias Configuration = Void + + public let store: NewKeyInvitationStore + + public init(store: NewKeyInvitationStore = .shared) { + self.store = store + } + + /// Provide the cached result if value has been fetched. + public func cachedValue(for id: URL) -> NewKey.Invitation? { + return store.cache[id] + } + + /// Provide sorted and filtered results. + public func fetch(configuration: Configuration = ()) -> [URL] { + do { + return try store + .fetchDocuments() + .sorted { $0.absoluteString < $1.absoluteString } + } + catch { + assertionFailure("Unable to fetch files \(error)") + return [] + } + } + + /// Asyncronously load the specified item. + public func load(_ url: URL) async -> Result { + do { + let value = try await store.load(url) + return .success(value) + } catch { + return .failure(error) + } + } + } +} diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index bbf01244..f483752e 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -27,13 +27,13 @@ public final class Store: ObservableObject { public internal(set) var state: DarwinBluetoothState = .unknown @Published - public var isScanning = false + public internal(set) var isScanning = false @Published - public var peripherals = [NativePeripheral: ScanData]() + public internal(set) var peripherals = [NativePeripheral: ScanData]() @Published - public var lockInformation = [NativePeripheral: LockInformation]() + public internal(set) var lockInformation = [NativePeripheral: LockInformation]() public lazy var central = NativeCentral.shared @@ -45,6 +45,8 @@ public final class Store: ObservableObject { internal lazy var fileManager: FileManager.Lock = .shared + public lazy var newKeyInvitations: NewKeyInvitationStore = .shared + public lazy var persistentContainer: NSPersistentContainer = .lock public lazy var managedObjectContext: NSManagedObjectContext = { diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 289a972e..18e20151 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -51,6 +51,9 @@ public struct LockDetailView: View { } .onAppear { reload() + } + .onDisappear { + } .alert(error: $error) .newPermissionSheet( @@ -152,8 +155,9 @@ private extension LockDetailView { showNewKeyModal = true } - func didCreateNewKey(_ newKey: NewKey.Invitation) { - + func didCreateNewKey(url: URL, invitation: NewKey.Invitation) { + showNewKeyModal = false + reload() } func reload() { diff --git a/Xcode/LockKit/View/LockRowView.swift b/Xcode/LockKit/View/LockRowView.swift index 1b16e59f..f3790721 100644 --- a/Xcode/LockKit/View/LockRowView.swift +++ b/Xcode/LockKit/View/LockRowView.swift @@ -45,7 +45,7 @@ public struct LockRowView: View { } } } - .padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)) + .padding(8) } public init( diff --git a/Xcode/LockKit/View/NavigationLink.swift b/Xcode/LockKit/View/NavigationLink.swift index 7b45f689..f5c7309b 100644 --- a/Xcode/LockKit/View/NavigationLink.swift +++ b/Xcode/LockKit/View/NavigationLink.swift @@ -21,7 +21,7 @@ public struct AppNavigationLink : View { if #available(macOS 13, iOS 16, tvOS 16, *) { navigationStackLink } else { - + navigationViewLink } #else navigationViewLink diff --git a/Xcode/LockKit/View/NewPermissionView.swift b/Xcode/LockKit/View/NewPermissionView.swift index 90af5f31..337cfc14 100644 --- a/Xcode/LockKit/View/NewPermissionView.swift +++ b/Xcode/LockKit/View/NewPermissionView.swift @@ -18,7 +18,7 @@ public struct NewPermissionView: View { public let id: UUID - private var completion: (NewKey.Invitation) -> () = { _ in } + private var completion: (URL, NewKey.Invitation) -> () = { (_, _) in assertionFailure() } @State private var permission: Permission = .anytime @@ -33,7 +33,7 @@ public struct NewPermissionView: View { id: UUID, name: String = "", permission: Permission = .anytime, - completion: @escaping (NewKey.Invitation) -> () + completion: @escaping (URL, NewKey.Invitation) -> () ) { self.id = id self.name = name @@ -77,13 +77,14 @@ private extension NewPermissionView { guard let peripheral = try await store.device(for: id) else { throw LockError.notInRange(lock: id) } - let newKey = try await store.newKey( + let invitation = try await store.newKey( for: peripheral, permission: permission, name: name ) + let url = try await store.newKeyInvitations.save(invitation) state = .editing - completion(newKey) + completion(url, invitation) } catch { state = .error(error.localizedDescription) log("⚠️ Error creating new key for \(id). \(error)") @@ -298,7 +299,7 @@ public extension View { for lock: UUID, isPresented: Binding, onDismiss: @escaping () -> (), - completion: @escaping (NewKey.Invitation) -> () + completion: @escaping (URL, NewKey.Invitation) -> () ) -> some View { return self.sheet(isPresented: isPresented, onDismiss: onDismiss) { #if os(iOS) @@ -338,7 +339,7 @@ public extension View { struct NewPermissionView_Previews: PreviewProvider { static var previews: some View { NavigationView { - NewPermissionView(id: UUID()) { _ in + NewPermissionView(id: UUID()) { (_, _) in } } diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 8841fad0..9804a693 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 6E10C90828E0006700703691 /* Hexadecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C90728E0006700703691 /* Hexadecimal.swift */; }; 6E10C90A28E03ED700703691 /* FetchKeysIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C90928E03ED700703691 /* FetchKeysIntent.swift */; }; 6E10C90C28E0518F00703691 /* NewKeyIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C90B28E0518F00703691 /* NewKeyIntent.swift */; }; + 6E10C90E28E0D97800703691 /* NewKeysAsyncFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E10C90D28E0D97800703691 /* NewKeysAsyncFetchRequest.swift */; }; 6E169A7828D9A34F008545EC /* NavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7728D9A34F008545EC /* NavigationLink.swift */; }; 6E169A7D28D9C097008545EC /* PermissionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7C28D9C097008545EC /* PermissionsView.swift */; }; 6E169A7F28D9C135008545EC /* NewPermissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E169A7E28D9C135008545EC /* NewPermissionView.swift */; }; @@ -164,6 +165,7 @@ 6E10C90728E0006700703691 /* Hexadecimal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Hexadecimal.swift; sourceTree = ""; }; 6E10C90928E03ED700703691 /* FetchKeysIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchKeysIntent.swift; sourceTree = ""; }; 6E10C90B28E0518F00703691 /* NewKeyIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyIntent.swift; sourceTree = ""; }; + 6E10C90D28E0D97800703691 /* NewKeysAsyncFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeysAsyncFetchRequest.swift; sourceTree = ""; }; 6E169A7728D9A34F008545EC /* NavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationLink.swift; sourceTree = ""; }; 6E169A7C28D9C097008545EC /* PermissionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsView.swift; sourceTree = ""; }; 6E169A7E28D9C135008545EC /* NewPermissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPermissionView.swift; sourceTree = ""; }; @@ -422,6 +424,7 @@ 6E21831228D80FDD00A622B3 /* JSON.swift */, 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */, 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */, + 6E10C90D28E0D97800703691 /* NewKeysAsyncFetchRequest.swift */, 6E84E53528DDE01F008CAE85 /* AssetExtractor.swift */, 6E21830F28D80DCD00A622B3 /* Keychain.swift */, 6E21831628D8116C00A622B3 /* NewKey.swift */, @@ -895,6 +898,7 @@ 6E21834328D8F3B500A622B3 /* ManagedObject.swift in Sources */, 6E84E54D28DF4A98008CAE85 /* MockCentral.swift in Sources */, 6E84E53628DDE01F008CAE85 /* AssetExtractor.swift in Sources */, + 6E10C90E28E0D97800703691 /* NewKeysAsyncFetchRequest.swift in Sources */, 6E21834028D8F3B500A622B3 /* RemoveKeyEventManagedObject.swift in Sources */, 6E21834A28D90F7A00A622B3 /* LocalAuthentication.swift in Sources */, 6E84E54F28DF4A98008CAE85 /* MockScanData.swift in Sources */, diff --git a/Xcode/SmartLock/View/KeysView.swift b/Xcode/SmartLock/View/KeysView.swift index 8b28cc38..3db57a7e 100644 --- a/Xcode/SmartLock/View/KeysView.swift +++ b/Xcode/SmartLock/View/KeysView.swift @@ -13,26 +13,53 @@ struct KeysView: View { @EnvironmentObject var store: Store + @AsyncFetchRequest( + dataSource: NewKeyInvitationStore.DataSource(store: .shared) + ) + var newKeys: AsyncFetchedResults + var body: some View { - StateView(items: items) + StateView( + locks: locks, + newKeys: newKeys.lazy.map({ + switch $0 { + case let .loading(url): + return .init(id: url, state: .loading) + case let .failure(url, error): + return .init(id: url, state: .error(error.localizedDescription)) + case let .success(url, invitation): + let name = store.applicationData.locks[invitation.lock]?.name ?? invitation.lock.uuidString + return .init(id: url, state: .invitation(invitation, name)) + } + }), + deleteNewKey: deleteNewKey + ) } } private extension KeysView { - var items: [Item] { + var locks: [Lock] { store.applicationData.locks .lazy .sorted(by: { $0.value.key.created < $1.value.key.created }) - .map { Item(id: $0.key, cache: $0.value) } + .map { Lock(id: $0.key, cache: $0.value) } + } + + func deleteNewKey(_ url: URL) { + log("Delete \(url)") } } extension KeysView { - struct StateView: View { + struct StateView : View where NewKeys: RandomAccessCollection, NewKeys.Element == Invitation { + + let locks: [Lock] + + let newKeys: NewKeys - let items: [Item] + let deleteNewKey: (URL) -> () var body: some View { list @@ -44,11 +71,59 @@ extension KeysView { private extension KeysView.StateView { var list: some View { - List(items) { (item) in - NavigationLink(destination: { - LockDetailView(id: item.id) - }, label: { - LockRowView(item) + List { + ForEach(locks) { + view(for: $0) + } + if newKeys.isEmpty == false { + Section("Pending") { + ForEach(newKeys) { + view(for: $0) + } + .onDelete { deleteNewKey(for: $0) } + } + } + } + } + + func deleteNewKey(for indexPath: IndexSet) { + //newKeys[indexPath[0]] + } + + func view(for item: KeysView.Lock) -> some View { + AppNavigationLink(id: .lock(item.id)) { + LockRowView( + image: .permission(item.cache.key.permission.type), + title: item.cache.name, + subtitle: item.cache.key.permission.type.localizedText + ) + } + } + + func view(for item: KeysView.Invitation) -> some View { + switch item.state { + case .loading: + return AnyView( + LockRowView( + image: .loading, + title: NSLocalizedString("Loading...", comment: "") + ) + ) + case let .error(error): + return AnyView( + LockRowView( + image: .emoji("⚠️"), + title: "Unable to load \(item.id.lastPathComponent).", + subtitle: error + ) + ) + case let .invitation(invitation, lockName): + return AnyView(AppNavigationLink(id: .key(.newKey(invitation.key))) { // FIXME: Show invitation view + LockRowView( + image: .permission(invitation.key.permission.type), + title: invitation.key.name, + subtitle: "\(lockName) - \(invitation.key.permission.localizedText)" + ) }) } } @@ -56,22 +131,41 @@ private extension KeysView.StateView { extension KeysView { - struct Item: Identifiable, Equatable { + enum Item { + case lock(Lock) + case invitation(Invitation) + } + + struct Lock: Identifiable, Equatable { let id: UUID let cache: LockCache } + + struct Invitation: Identifiable { + + let id: URL + + let state: InvitationState + } + + enum InvitationState { + case loading + case invitation(NewKey.Invitation, String) + case error(String) + } } -extension LockRowView { +extension KeysView.Item: Identifiable { - init(_ item: KeysView.Item) { - self.init( - image: .permission(item.cache.key.permission.type), - title: item.cache.name, - subtitle: item.cache.key.permission.type.localizedText - ) + var id: String { + switch self { + case let .lock(value): + return value.id.uuidString + case let .invitation(value): + return value.id.absoluteString + } } } @@ -79,9 +173,10 @@ extension LockRowView { #if DEBUG struct KeysView_Previews: PreviewProvider { + static var previews: some View { - NavigationView { - KeysView.StateView(items: [ + PreviewView( + locks: [ .init( id: UUID(), cache: LockCache( @@ -108,7 +203,57 @@ struct KeysView_Previews: PreviewProvider { ) ) ) - ]) + ], + newKeys: [ + .init( + id: URL(fileURLWithPath: "/tmp/newKey-\(UUID()).ekey"), + state: .loading + ), + .init( + id: URL(fileURLWithPath: "/tmp/newKey-\(UUID(uuidString: "879F5EF6-2369-47B4-9A6D-F2413C06EF14")!).ekey"), + state: .invitation(try! JSONDecoder().decode(NewKey.Invitation.self, from: Data(#"{"key":{"id":"879F5EF6-2369-47B4-9A6D-F2413C06EF14","created":685436924.88646305,"name":"Anytime Key","permission":{"type":"anytime"},"expiration":685523324.88646305},"lock":"DD944B0B-3C40-4524-9C71-7A7FE23DCB8D","secret":"dgARK0MXd4Em6IcuRUUItrq3rZcAPcpSQ5LwzzM3c9I="}"#.utf8)), "My lock") + ), + .init( + id: URL(fileURLWithPath: "/tmp/newKey-\(UUID()).ekey"), + state: .loading + ), + .init( + id: URL(fileURLWithPath: "/tmp/newKey-\(UUID()).ekey"), + state: .error("Unable to read file.") + ) + ] + ) + } + + struct PreviewView: View { + + @State + var locks: [KeysView.Lock] + + @State + var newKeys: [KeysView.Invitation] + + var body: some View { + NavigationView { + KeysView.StateView( + locks: locks, + newKeys: newKeys, + deleteNewKey: { url in + if let index = newKeys.firstIndex(where: { $0.id == url }) { + newKeys.remove(at: index) + } + } + ) + .refreshable { + Task { + try await Task.sleep(timeInterval: 1.0) + newKeys[0] = .init( + id: URL(fileURLWithPath: "/tmp/newKey-\(UUID(uuidString: "879F5EF6-2369-47B4-9A6D-F2413C06EF14")!).ekey"), + state: .invitation(try! JSONDecoder().decode(NewKey.Invitation.self, from: Data(#"{"key":{"id":"879F5EF6-2369-47B4-9A6D-F2413C06EF14","created":685436924.88646305,"name":"Anytime Key","permission":{"type":"anytime"},"expiration":685523324.88646305},"lock":"DD944B0B-3C40-4524-9C71-7A7FE23DCB8D","secret":"dgARK0MXd4Em6IcuRUUItrq3rZcAPcpSQ5LwzzM3c9I="}"#.utf8)), "My lock") + ) + } + } + } } } } From 14afd727653bf89d5ce725536c09443464908c68 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 25 Sep 2022 17:44:58 -0700 Subject: [PATCH 170/229] [App] Add sharing key files for `PermissionsView` --- Xcode/LockKit/View/PermissionsView.swift | 119 ++++++++++++++++++----- 1 file changed, 96 insertions(+), 23 deletions(-) diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index aae70d86..0c5a3cc3 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -16,6 +16,9 @@ public struct PermissionsView: View { @Environment(\.managedObjectContext) public var managedObjectContext + @StateObject + public var fileStore: NewKeyInvitationStore = Store.shared.newKeyInvitations + /// Identifier of lock public let id: UUID @@ -69,19 +72,22 @@ public struct PermissionsView: View { @State private var showNewKeyModal = false - @State - private var newKeyInvitation: NewKey.Invitation? - public var body: some View { StateView( keys: keys.lazy.compactMap { Key(managedObject: $0) }, newKeys: newKeys.lazy.compactMap { NewKey(managedObject: $0) }, + invitations: invitations, reload: reload ) .onAppear { self.keys.nsPredicate = predicate self.newKeys.nsPredicate = predicate } + .onDisappear { + Task { + await self.reloadTask?.cancel() + } + } .toolbar { ToolbarItem(placement: .primaryAction) { if canCreateNewKey { @@ -122,6 +128,17 @@ private extension PermissionsView { ) } + var invitations: [UUID: URL] { + let cache = fileStore.cache + var invitations = [UUID: URL]() + invitations.reserveCapacity(cache.count) + for (url, invitation) in cache { + invitations[invitation.key.id] = url + } + assert(invitations.count == cache.count) + return invitations + } + func reload() { activityIndicator = true Task { @@ -145,7 +162,21 @@ private extension PermissionsView { log("⚠️ Error loading keys for \(id). \(error)") } } - + Task { + do { + let urls = try fileStore.fetchDocuments() + for url in urls { + guard fileStore.cache[url] == nil, + let invitation = try? await fileStore.load(url) else { + continue + } + log("Loaded key \(invitation.key.name) \(invitation.lock) at \(url.lastPathComponent)") + } + } catch { + log("⚠️ Error loading pending keys invitations. \(error)") + assertionFailure() + } + } } } @@ -161,16 +192,12 @@ private extension PermissionsView { showNewKeyModal = true } - func didCreateNewKey(_ newKey: NewKey.Invitation) { + func didCreateNewKey(url: URL, invitation: NewKey.Invitation) { // hide modal showNewKeyModal = false - // show popover + // reload Task { try? await Task.sleep(timeInterval: 0.2) - self.newKeyInvitation = newKey - } - Task { - try? await Task.sleep(timeInterval: 1.0) // reload pending keys reload() } @@ -185,6 +212,8 @@ internal extension PermissionsView { let newKeys: NewKeys + let invitations: [UUID: URL] + let reload: () -> () var body: some View { @@ -196,7 +225,7 @@ internal extension PermissionsView { if newKeys.isEmpty == false { Section("Pending") { ForEach(newKeys) { - row(for: $0) + row(for: $0, invitationURL: invitations[$0.id]) } .onDelete(perform: deleteNewKey) } @@ -229,20 +258,51 @@ private extension PermissionsView.StateView { }) } - func row(for item: NewKey) -> some View { + func row(for item: NewKey, invitationURL: URL?) -> some View { AppNavigationLink(id: .key(.newKey(item)), label: { - LockRowView( - image: .permission(item.permission.type), - title: item.name, - subtitle: item.permission.localizedText + "\n" + "Expires " + PermissionsView.relativeDateTimeFormatter.localizedString(for: item.expiration, relativeTo: Date()), - trailing: ( - PermissionsView.dateFormatter.string(from: item.created), - PermissionsView.timeFormatter.string(from: item.created) - ) - ) + HStack(alignment: .center, spacing: 8) { + row(for: item, showDate: invitationURL == nil) + if let url = invitationURL { + Spacer() + if #available(macOS 13, iOS 16, *) { + AnyView( + ShareLink( + item: url, + subject: Text("\(item.name)"), + message: Text("Share this key"), + label: { shareImage } + ) + .buttonStyle(.plain) + ) + } else { + AnyView(Button(action: { + + }, label: { shareImage })) + .buttonStyle(.plain) + } + } + } }) } + func row(for item: NewKey, showDate: Bool) -> some View { + LockRowView( + image: .permission(item.permission.type), + title: item.name, + subtitle: item.permission.localizedText + "\n" + "Expires " + PermissionsView.relativeDateTimeFormatter.localizedString(for: item.expiration, relativeTo: Date()), + trailing: showDate ? ( + PermissionsView.dateFormatter.string(from: item.created), + PermissionsView.timeFormatter.string(from: item.created) + ) : nil + ) + } + + var shareImage: some View { + Image(systemSymbol: .squareAndArrowUp) + .foregroundColor(.blue) + .padding(8) + } + func destination(for item: Key) -> some View { KeyDetailView(key: .key(item)) } @@ -261,8 +321,9 @@ private extension PermissionsView.StateView { } // MARK: - Preview - +/* struct PermissionsView_Previews: PreviewProvider { + static var previews: some View { NavigationView { PermissionsView.StateView( @@ -300,15 +361,27 @@ struct PermissionsView_Previews: PreviewProvider { ], newKeys: [ NewKey( - id: UUID(), + id: UUID(uuidString: "ED6DE87A-D0AF-421B-912D-3400A60EB294")!, name: "Key 4", permission: .anytime, created: Date() - 60 * 60 * 2, expiration: Date() + (60 * 60 * 24 * 1) + 10 + ), + NewKey( + id: UUID()!, + name: "Key 5", + permission: .anytime, + created: Date() - 60 * 60 * 1, + expiration: Date() + (60 * 60 * 24 * 1) + 10 ) ], + invitations: [ + UUID(uuidString: "ED6DE87A-D0AF-421B-912D-3400A60EB294")! : + URL(fileURLWithPath: "/tmp/newKey-\(UUID(uuidString: "ED6DE87A-D0AF-421B-912D-3400A60EB294")!).ekey") + ], reload: { } ) } } } +*/ From d56790f1fc4516d0d6158586096674fb57dcb2a8 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 25 Sep 2022 19:46:05 -0700 Subject: [PATCH 171/229] [App] Fixed `KeysView` --- Xcode/LockKit/Model/AsyncFetchRequest.swift | 76 +++++-------------- .../Model/NewKeysAsyncFetchRequest.swift | 7 +- Xcode/LockKit/View/LockRowView.swift | 28 ++++--- Xcode/LockKit/View/PermissionsView.swift | 14 +--- Xcode/SmartLock/View/KeysView.swift | 26 +++++-- 5 files changed, 67 insertions(+), 84 deletions(-) diff --git a/Xcode/LockKit/Model/AsyncFetchRequest.swift b/Xcode/LockKit/Model/AsyncFetchRequest.swift index 6b8d3674..039ba789 100644 --- a/Xcode/LockKit/Model/AsyncFetchRequest.swift +++ b/Xcode/LockKit/Model/AsyncFetchRequest.swift @@ -8,43 +8,7 @@ import Foundation import SwiftUI -@propertyWrapper -@MainActor -public struct AsyncFetchRequest { - - internal let dataSource: DataSource - - internal let configuration: DataSource.Configuration - - public init( - dataSource: DataSource, - configuration: DataSource.Configuration - ) { - self.dataSource = dataSource - self.configuration = configuration - } - - public var wrappedValue: AsyncFetchedResults { - return .init( - dataSource: dataSource, - configuration: configuration - ) - } -} - -public extension AsyncFetchRequest where DataSource.Configuration == Void { - - init( - dataSource: DataSource - ) { - self.init( - dataSource: dataSource, - configuration: () - ) - } -} - -public protocol AsyncFetchDataSource { +public protocol AsyncFetchDataSource: ObservableObject { associatedtype ID: Hashable @@ -64,36 +28,35 @@ public protocol AsyncFetchDataSource { func load(_ id: ID) async -> Result } -extension AsyncFetchRequest: DynamicProperty { - - public mutating func update() { - print(#function) - } -} - @MainActor public struct AsyncFetchedResults { - //@ObservedObject + @ObservedObject internal var dataSource: DataSource internal var configuration: DataSource.Configuration - @State - internal var results = [DataSource.ID]() + @Binding + internal var results: [DataSource.ID] - @State - internal var tasks = [DataSource.ID: Task]() + @Binding + internal var tasks: [DataSource.ID: Task] - @State - internal var errors = [DataSource.ID: DataSource.Failure]() + @Binding + internal var errors: [DataSource.ID: DataSource.Failure] public init( dataSource: DataSource, - configuration: DataSource.Configuration + configuration: DataSource.Configuration, + results: Binding<[DataSource.ID]>, + tasks: Binding<[DataSource.ID: Task]>, + errors: Binding<[DataSource.ID: DataSource.Failure]> ) { self.dataSource = dataSource self.configuration = configuration + self._results = results + self._tasks = tasks + self._errors = errors } } @@ -156,8 +119,12 @@ private extension AsyncFetchedResults.Element { extension AsyncFetchedResults: RandomAccessCollection { public var count: Int { - // fetch - results = dataSource.fetch(configuration: configuration) + Task { + let results = dataSource.fetch(configuration: configuration) + if self.results != results { + self.results = results + } + } return results.count } @@ -202,7 +169,6 @@ extension AsyncFetchedResults: RandomAccessCollection { if let error = errors[id] { return .failure(id, error) } else { - assert(tasks[id] != nil) return .loading(id) } } diff --git a/Xcode/LockKit/Model/NewKeysAsyncFetchRequest.swift b/Xcode/LockKit/Model/NewKeysAsyncFetchRequest.swift index b0edda45..d92841ea 100644 --- a/Xcode/LockKit/Model/NewKeysAsyncFetchRequest.swift +++ b/Xcode/LockKit/Model/NewKeysAsyncFetchRequest.swift @@ -6,11 +6,12 @@ // import Foundation +import Combine import CoreLock public extension NewKeyInvitationStore { - struct DataSource: AsyncFetchDataSource { + class DataSource: AsyncFetchDataSource { public typealias Configuration = Void @@ -20,6 +21,10 @@ public extension NewKeyInvitationStore { self.store = store } + public var objectWillChange: ObservableObjectPublisher { + store.objectWillChange + } + /// Provide the cached result if value has been fetched. public func cachedValue(for id: URL) -> NewKey.Invitation? { return store.cache[id] diff --git a/Xcode/LockKit/View/LockRowView.swift b/Xcode/LockKit/View/LockRowView.swift index f3790721..52b59393 100644 --- a/Xcode/LockKit/View/LockRowView.swift +++ b/Xcode/LockKit/View/LockRowView.swift @@ -19,20 +19,24 @@ public struct LockRowView: View { public let trailing: (String, String)? public var body: some View { - HStack(alignment: .center, spacing: 16) { - VStack { - ImageView(image: image) - .frame(width: 50, height: 50, alignment: .center) - } - VStack(alignment: .leading, spacing: 8) { - Text(verbatim: title) - .font(.system(size: 19)) - if let subtitle = subtitle { - Text(verbatim: subtitle) - .font(.system(size: 14)) - .foregroundColor(.gray) + HStack(alignment: .center, spacing: 3) { + // + HStack(alignment: .center, spacing: 16) { + VStack { + ImageView(image: image) + .frame(width: 50, height: 50, alignment: .center) + } + VStack(alignment: .leading, spacing: 8) { + Text(verbatim: title) + .font(.system(size: 19)) + if let subtitle = subtitle { + Text(verbatim: subtitle) + .font(.system(size: 14)) + .foregroundColor(.gray) + } } } + // if let trailing = self.trailing { Spacer(minLength: 1) VStack(alignment: .trailing, spacing: 8) { diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index 0c5a3cc3..41d08cf0 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -321,7 +321,8 @@ private extension PermissionsView.StateView { } // MARK: - Preview -/* + +#if DEBUG struct PermissionsView_Previews: PreviewProvider { static var previews: some View { @@ -362,17 +363,10 @@ struct PermissionsView_Previews: PreviewProvider { newKeys: [ NewKey( id: UUID(uuidString: "ED6DE87A-D0AF-421B-912D-3400A60EB294")!, - name: "Key 4", + name: "Anytime Key", permission: .anytime, created: Date() - 60 * 60 * 2, expiration: Date() + (60 * 60 * 24 * 1) + 10 - ), - NewKey( - id: UUID()!, - name: "Key 5", - permission: .anytime, - created: Date() - 60 * 60 * 1, - expiration: Date() + (60 * 60 * 24 * 1) + 10 ) ], invitations: [ @@ -384,4 +378,4 @@ struct PermissionsView_Previews: PreviewProvider { } } } -*/ +#endif diff --git a/Xcode/SmartLock/View/KeysView.swift b/Xcode/SmartLock/View/KeysView.swift index 3db57a7e..cc30a30e 100644 --- a/Xcode/SmartLock/View/KeysView.swift +++ b/Xcode/SmartLock/View/KeysView.swift @@ -13,10 +13,14 @@ struct KeysView: View { @EnvironmentObject var store: Store - @AsyncFetchRequest( - dataSource: NewKeyInvitationStore.DataSource(store: .shared) - ) - var newKeys: AsyncFetchedResults + @State + private var invitationFiles = [URL]() + + @State + private var fileTasks = [URL: Task]() + + @State + private var fileErrors = [URL: Error]() var body: some View { StateView( @@ -34,6 +38,9 @@ struct KeysView: View { }), deleteNewKey: deleteNewKey ) + .onAppear { + reload() + } } } @@ -46,9 +53,17 @@ private extension KeysView { .map { Lock(id: $0.key, cache: $0.value) } } + var newKeys: AsyncFetchedResults { + .init(dataSource: .init(store: store.newKeyInvitations), configuration: (), results: $invitationFiles, tasks: $fileTasks, errors: $fileErrors) + } + func deleteNewKey(_ url: URL) { log("Delete \(url)") } + + func reload() { + newKeys.reload() + } } extension KeysView { @@ -62,8 +77,7 @@ extension KeysView { let deleteNewKey: (URL) -> () var body: some View { - list - .navigationTitle("Keys") + list.navigationTitle("Keys") } } } From c0d7a05ee546f07bd3b45ab659f2f1a2ff48ba30 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 25 Sep 2022 20:07:26 -0700 Subject: [PATCH 172/229] [App] Added `ScanResultsAsyncDataSource` --- .../Model/ScanResultsAsyncDataSource.swift | 51 +++++++++++++++++++ Xcode/SmartLock.xcodeproj/project.pbxproj | 4 ++ 2 files changed, 55 insertions(+) create mode 100644 Xcode/LockKit/Model/ScanResultsAsyncDataSource.swift diff --git a/Xcode/LockKit/Model/ScanResultsAsyncDataSource.swift b/Xcode/LockKit/Model/ScanResultsAsyncDataSource.swift new file mode 100644 index 00000000..e9a8e6b8 --- /dev/null +++ b/Xcode/LockKit/Model/ScanResultsAsyncDataSource.swift @@ -0,0 +1,51 @@ +// +// ScanResultsAsyncFetchRequest.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/25/22. +// + +import Foundation +import Combine +import GATT +import CoreLock + +@MainActor +public final class ScanResultsAsyncDataSource: AsyncFetchDataSource { + + public typealias Configuration = Void + + public let store: Store + + public let objectWillChange: ObservableObjectPublisher + + public init(store: Store = .shared) { + self.store = store + self.objectWillChange = store.objectWillChange + } + + /// Provide the cached result if value has been fetched. + public func cachedValue(for id: NativePeripheral) -> LockInformation? { + return store.lockInformation[id] + } + + /// Provide sorted and filtered results. + public func fetch(configuration: Configuration = ()) -> [NativePeripheral] { + return store.peripherals.keys + .lazy + .sorted(by: { store.lockInformation[$0]?.id.description ?? "" > store.lockInformation[$1]?.id.description ?? "" }) + .sorted(by: { + store.applicationData.locks[store.lockInformation[$0]?.id ?? UUID()]?.key.created ?? .distantFuture > store.applicationData.locks[store.lockInformation[$1]?.id ?? UUID()]?.key.created ?? .distantFuture }) + .sorted(by: { $0.description < $1.description }) + } + + /// Asyncronously load the specified item. + public nonisolated func load(_ id: NativePeripheral) async -> Result { + do { + let value = try await store.readInformation(for: id) + return .success(value) + } catch { + return .failure(error) + } + } +} diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 9804a693..b0b73013 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -123,6 +123,7 @@ 6EA776A028D707FE00018FA3 /* LockKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 6EA7769F28D707FE00018FA3 /* LockKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6EA776A328D707FE00018FA3 /* LockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; }; 6EA776A428D707FE00018FA3 /* LockKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 6EBD590F28E14A7F00CC3852 /* ScanResultsAsyncDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBD590E28E14A7F00CC3852 /* ScanResultsAsyncDataSource.swift */; }; 6ED248CC28D7A3BF00F78D07 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ED248CB28D7A3BF00F78D07 /* ActivityIndicatorView.swift */; }; /* End PBXBuildFile section */ @@ -274,6 +275,7 @@ 6EA7769128D7061600018FA3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 6EA7769D28D707FE00018FA3 /* LockKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LockKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6EA7769F28D707FE00018FA3 /* LockKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LockKit.h; sourceTree = ""; }; + 6EBD590E28E14A7F00CC3852 /* ScanResultsAsyncDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanResultsAsyncDataSource.swift; sourceTree = ""; }; 6ED248CB28D7A3BF00F78D07 /* ActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -425,6 +427,7 @@ 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */, 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */, 6E10C90D28E0D97800703691 /* NewKeysAsyncFetchRequest.swift */, + 6EBD590E28E14A7F00CC3852 /* ScanResultsAsyncDataSource.swift */, 6E84E53528DDE01F008CAE85 /* AssetExtractor.swift */, 6E21830F28D80DCD00A622B3 /* Keychain.swift */, 6E21831628D8116C00A622B3 /* NewKey.swift */, @@ -870,6 +873,7 @@ 6E21831828D8116C00A622B3 /* NewKey.swift in Sources */, 6E21835F28D9516B00A622B3 /* CloudApplicationData.swift in Sources */, 6E8BBFDC28DCC8F300F03735 /* AsyncFetchRequest.swift in Sources */, + 6EBD590F28E14A7F00CC3852 /* ScanResultsAsyncDataSource.swift in Sources */, 6E21831028D80DCD00A622B3 /* Keychain.swift in Sources */, 6E21833C28D8F3B500A622B3 /* KeyManagedObject.swift in Sources */, 6E21830128D7C37500A622B3 /* Error.swift in Sources */, From 84ebdb1fbcedb2d4010f3b17cb8170d32dd15956 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 25 Sep 2022 21:20:51 -0700 Subject: [PATCH 173/229] [App] Fixed reading information for `NearbyDevicesView` --- Xcode/SmartLock/View/NearbyDevicesView.swift | 97 +++++++++++--------- 1 file changed, 55 insertions(+), 42 deletions(-) diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index 9270d17c..73a60200 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -13,16 +13,25 @@ struct NearbyDevicesView: View { @EnvironmentObject var store: Store - @SwiftUI.State + @State private var scanTask: TaskQueue.PendingTask? + @State + private var peripherals = [NativePeripheral]() + + @State + private var readInformationTasks = [NativePeripheral: Task]() + + @State + private var readInformationError = [NativePeripheral: Error]() + var body: some View { StateView( state: state, - items: items, + items: scanResults.map { item(for: $0) }, toggleScan: toggleScan, destination: { (item) in - if let information = store.lockInformation.first(where: { $0.key.id == item.id })?.value { + if let information = store.lockInformation[item.id] { return AnyView(LockDetailView(id: information.id)) } else { return AnyView(EmptyView()) @@ -66,43 +75,37 @@ private extension NearbyDevicesView { } } - var peripherals: [NativePeripheral] { - store.peripherals.keys - .lazy - .sorted(by: { store.lockInformation[$0]?.id.description ?? "" > store.lockInformation[$1]?.id.description ?? "" }) - .sorted(by: { - store.applicationData.locks[store.lockInformation[$0]?.id ?? UUID()]?.key.created ?? .distantFuture > store.applicationData.locks[store.lockInformation[$1]?.id ?? UUID()]?.key.created ?? .distantFuture }) - .sorted(by: { $0.description < $1.description }) + var scanResults: AsyncFetchedResults { + .init(dataSource: .init(store: store), configuration: (), results: $peripherals, tasks: $readInformationTasks, errors: $readInformationError) } - var state: State { - if store.state != .poweredOn { - return .bluetoothUnavailable - } else if store.isScanning { - return .scanning - } else { - return .stopScan - } - } - - var items: [Item] { - peripherals.map { item(for: $0) } - } - - func item(for peripheral: NativePeripheral) -> Item { - if let information = store.lockInformation[peripheral] { + func item(for element: AsyncFetchedResults.Element) -> NearbyDevicesView.Item { + switch element { + case let .loading(peripheral): + return .loading(peripheral) + case let .failure(peripheral, error): + return .error(peripheral, error.localizedDescription) + case let .success(peripheral, information): switch information.status { case .setup: - return .setup(peripheral.id, information.id) + return .setup(peripheral, information.id) default: if let lockCache = store[lock: information.id] { - return .key(peripheral.id, lockCache.name, lockCache.key.permission.type) + return .lock(peripheral, lockCache.name, lockCache.key.permission.type) } else { - return .unknown(peripheral.id, information.id) + return .unknown(peripheral, information.id) } } + } + } + + var state: ScanState { + if store.state != .poweredOn { + return .bluetoothUnavailable + } else if store.isScanning { + return .scanning } else { - return .loading(peripheral.id) + return .stopScan } } } @@ -111,7 +114,7 @@ extension NearbyDevicesView { struct StateView : View where Destination: View { - let state: State + let state: ScanState let items: [Item] @@ -145,9 +148,9 @@ private extension NearbyDevicesView.StateView { List { ForEach(items) { (item) in switch item { - case .loading, .unknown: + case .loading, .error, .unknown: LockRowView(item) - case .key, .setup: + case .lock, .setup: NavigationLink(destination: { destination(item) }, label: { @@ -181,7 +184,7 @@ private extension NearbyDevicesView.StateView { extension NearbyDevicesView { - enum State { + enum ScanState { case bluetoothUnavailable case scanning case stopScan @@ -191,22 +194,25 @@ extension NearbyDevicesView { extension NearbyDevicesView { enum Item { - case loading(NativeCentral.Peripheral.ID) - case setup(NativeCentral.Peripheral.ID, UUID) - case key(NativeCentral.Peripheral.ID, String, PermissionType) - case unknown(NativeCentral.Peripheral.ID, UUID) + case loading(NativePeripheral) + case error(NativePeripheral, String) + case setup(NativePeripheral, UUID) + case lock(NativePeripheral, String, PermissionType) + case unknown(NativePeripheral, UUID) } } extension NearbyDevicesView.Item: Identifiable { - var id: NativeCentral.Peripheral.ID { + var id: NativeCentral.Peripheral { switch self { + case let .error(id, _): + return id case let .loading(id): return id case let .setup(id, _): return id - case let .key(id, _, _): + case let .lock(id, _, _): return id case let .unknown(id, _): return id @@ -223,6 +229,12 @@ extension LockRowView { image: .loading, title: "Loading..." ) + case let .error(_, error): + self.init( + image: .emoji("⚠️"), + title: "Error", + subtitle: error + ) case let .unknown(_, id): self.init( image: .permission(.anytime), @@ -235,7 +247,7 @@ extension LockRowView { title: "Setup", subtitle: id.description ) - case let .key(_, name, type): + case let .lock(_, name, type): self.init( image: .permission(type), title: name, @@ -255,9 +267,10 @@ struct NearbyDevicesView_Previews: PreviewProvider { state: .scanning, items: [ .loading(.random), + .error(.random, LockError.notInRange(lock: UUID()).localizedDescription), .setup(.random, UUID()), .unknown(.random, UUID()), - .key(.random, "My lock", .admin) + .lock(.random, "My lock", .admin) ], toggleScan: { }, destination: { Text(verbatim: $0.id.description) } From 4fe1c8e5ccc9c04429c2d84d21ab153cf50c957b Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 25 Sep 2022 21:39:11 -0700 Subject: [PATCH 174/229] [App] Fixed `AsyncFetchDataSource` --- Xcode/LockKit/Model/AsyncFetchRequest.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Xcode/LockKit/Model/AsyncFetchRequest.swift b/Xcode/LockKit/Model/AsyncFetchRequest.swift index 039ba789..d83253c5 100644 --- a/Xcode/LockKit/Model/AsyncFetchRequest.swift +++ b/Xcode/LockKit/Model/AsyncFetchRequest.swift @@ -151,7 +151,8 @@ extension AsyncFetchedResults: RandomAccessCollection { return .success(id, cachedValue) } else { // async load - if tasks[id] == nil { + Task { + guard tasks[id] == nil else { return } tasks[id] = Task { // load let result = await dataSource.load(id) From 74bc2088f3d7f24cbd6f71281ac80eeb2a8ff51c Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 25 Sep 2022 23:19:35 -0700 Subject: [PATCH 175/229] [App] Updated `NewKeyInvitationView` --- Xcode/LockKit/View/KeyDetailView.swift | 155 +++++++++++------- Xcode/LockKit/View/NavigationLink.swift | 14 +- Xcode/LockKit/View/NewKeyInvitationView.swift | 55 ++++++- Xcode/LockKit/View/PermissionsView.swift | 20 +-- Xcode/SmartLock/View/KeysView.swift | 2 +- Xcode/SmartLock/View/SidebarView.swift | 1 - 6 files changed, 170 insertions(+), 77 deletions(-) diff --git a/Xcode/LockKit/View/KeyDetailView.swift b/Xcode/LockKit/View/KeyDetailView.swift index 89527f70..02975289 100644 --- a/Xcode/LockKit/View/KeyDetailView.swift +++ b/Xcode/LockKit/View/KeyDetailView.swift @@ -10,87 +10,125 @@ import CoreLock public struct KeyDetailView: View { + @EnvironmentObject + public var store: Store + public let key: Value - @State - var showID = false + public let lock: UUID - private static let dateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .short - formatter.timeStyle = .short - return formatter - }() + public init(key: Value, lock: UUID) { + self.key = key + self.lock = lock + } - private var titleWidth: CGFloat { - 100 + public var body: some View { + StateView( + key: key, + lock: lockText, + showID: false + ) } +} + +private extension KeyDetailView { - public init(key: Value) { - self.key = key + var lockText: String { + return store.applicationData.locks[lock]?.name ?? lock.description } +} + +extension KeyDetailView { - public var body: some View { - ScrollView { - VStack(alignment: .leading, spacing: 20) { - HStack { - Spacer() - PermissionIconView(permission: key.permission.type) - .frame(width: 150, height: 150, alignment: .center) - .padding(30) - Spacer() - } + struct StateView: View { + + let key: Value + + let lock: String + + @State + var showID = false + + private static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .short + return formatter + }() + + private var titleWidth: CGFloat { + 100 + } + + var body: some View { + ScrollView { VStack(alignment: .leading, spacing: 20) { - // info - if showID { + HStack { + Spacer() + PermissionIconView(permission: key.permission.type) + .frame(width: 150, height: 150, alignment: .center) + .padding(30) + Spacer() + } + VStack(alignment: .leading, spacing: 20) { + // info + if showID { + HStack { + Text("Key") + .frame(width: titleWidth, height: nil, alignment: .leading) + .font(.body) + .foregroundColor(.gray) + Text(verbatim: key.id.description) + } + } HStack { - Text("Key") + Text("Lock") .frame(width: titleWidth, height: nil, alignment: .leading) .font(.body) .foregroundColor(.gray) - Text(verbatim: key.id.description) + Text(verbatim: lock) } - } - HStack { - Text("Type") - .frame(width: titleWidth, height: nil, alignment: .leading) - .font(.body) - .foregroundColor(.gray) - if let schedule = key.permission.schedule { - AppNavigationLink(id: .keySchedule(schedule), label: { - HStack { - Text(verbatim: key.permission.localizedText) - .foregroundColor(.primary) - Image(systemName: "chevron.right") - } - }) - } else { - Text(verbatim: key.permission.localizedText) - .foregroundColor(.primary) + HStack { + Text("Type") + .frame(width: titleWidth, height: nil, alignment: .leading) + .font(.body) + .foregroundColor(.gray) + if let schedule = key.permission.schedule { + AppNavigationLink(id: .keySchedule(schedule), label: { + HStack { + Text(verbatim: key.permission.localizedText) + .foregroundColor(.primary) + Image(systemName: "chevron.right") + } + }) + } else { + Text(verbatim: key.permission.localizedText) + .foregroundColor(.primary) + } } - } - HStack { - Text("Created") - .frame(width: titleWidth, height: nil, alignment: .leading) - .font(.body) - .foregroundColor(.gray) - Text(verbatim: Self.dateFormatter.string(from: key.created)) - } - if let expiration = key.expiration { HStack { - Text("Expiration") + Text("Created") .frame(width: titleWidth, height: nil, alignment: .leading) .font(.body) .foregroundColor(.gray) - Text(verbatim: Self.dateFormatter.string(from: expiration)) + Text(verbatim: Self.dateFormatter.string(from: key.created)) + } + if let expiration = key.expiration { + HStack { + Text("Expiration") + .frame(width: titleWidth, height: nil, alignment: .leading) + .font(.body) + .foregroundColor(.gray) + Text(verbatim: Self.dateFormatter.string(from: expiration)) + } } } } + .padding(20) + .buttonStyle(.plain) } - .padding(20) - .buttonStyle(.plain) + .navigationTitle(Text(verbatim: key.name)) } - .navigationTitle(Text(verbatim: key.name)) } } @@ -165,7 +203,8 @@ struct KeyDetailView_Previews: PreviewProvider { ForEach(keys) { key in NavigationView { KeyDetailView( - key: key + key: key, + lock: UUID() ) } .previewDisplayName(key.name) diff --git a/Xcode/LockKit/View/NavigationLink.swift b/Xcode/LockKit/View/NavigationLink.swift index f5c7309b..15d950c2 100644 --- a/Xcode/LockKit/View/NavigationLink.swift +++ b/Xcode/LockKit/View/NavigationLink.swift @@ -57,7 +57,8 @@ public enum AppNavigationLinkID: Hashable { case lock(UUID) case events(UUID, LockEvent.Predicate?) case permissions(UUID) - case key(KeyDetailView.Value) + case key(UUID, KeyDetailView.Value) + case newKeyInvitation(NewKey.Invitation) case keySchedule(Permission.Schedule) // view only } @@ -67,6 +68,7 @@ public enum AppNavigationLinkType: String { case events case permissions case key + case newKeyInvitation case keySchedule } @@ -84,6 +86,8 @@ public extension AppNavigationLinkID { return .key case .keySchedule: return .keySchedule + case .newKeyInvitation: + return .newKeyInvitation } } } @@ -110,14 +114,18 @@ public struct AppNavigationDestinationView: View { AnyView( PermissionsView(id: id) ) - case let .key(key): + case let .key(lock, key): AnyView( - KeyDetailView(key: key) + KeyDetailView(key: key, lock: lock) ) case let .keySchedule(schedule): AnyView( PermissionScheduleView(schedule: schedule) ) + case let .newKeyInvitation(invitation): + AnyView( + NewKeyInvitationView(invitation: invitation) + ) } } } diff --git a/Xcode/LockKit/View/NewKeyInvitationView.swift b/Xcode/LockKit/View/NewKeyInvitationView.swift index 18183961..54735d51 100644 --- a/Xcode/LockKit/View/NewKeyInvitationView.swift +++ b/Xcode/LockKit/View/NewKeyInvitationView.swift @@ -13,20 +13,67 @@ public struct NewKeyInvitationView: View { @EnvironmentObject public var store: Store - public let newKey: NewKey.Invitation + public let invitation: NewKey.Invitation - public init(newKey: NewKey.Invitation) { - self.newKey = newKey + @State + private var activityIndicator = false + + @State + private var pendingTask: TaskQueue.PendingTask? + + public init(invitation: NewKey.Invitation) { + self.invitation = invitation } public var body: some View { - Text("") + KeyDetailView( + key: .newKey(invitation.key), + lock: invitation.lock + ) + .toolbar { + ToolbarItem(placement: .primaryAction) { + if store.applicationData.locks[invitation.lock] == nil { + if activityIndicator { + ProgressView() + .progressViewStyle(.circular) + } else { + Button(action: { + accept() + }, label: { + Text("Accept") + }) + } + } + } + } } } internal extension NewKeyInvitationView { + func accept() { + // request name modal + createNewKey("My lock") + } + func createNewKey(_ name: String) { + activityIndicator = true + Task { + await pendingTask?.cancel() + await pendingTask = Task.bluetooth { + activityIndicator = true + defer { Task { await MainActor.run { activityIndicator = false } } } + guard await store.central.state == .poweredOn else { + return + } + do { + try await store.confirm(invitation, name: name) + } catch { + log("⚠️ Error creating new key for \(invitation.lock). \(error)") + } + } + } + } } #if DEBUG diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index 41d08cf0..905181e6 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -74,6 +74,7 @@ public struct PermissionsView: View { public var body: some View { StateView( + lock: id, keys: keys.lazy.compactMap { Key(managedObject: $0) }, newKeys: newKeys.lazy.compactMap { NewKey(managedObject: $0) }, invitations: invitations, @@ -208,6 +209,8 @@ internal extension PermissionsView { struct StateView : View where Keys: RandomAccessCollection, Keys.Element == Key, NewKeys: RandomAccessCollection, NewKeys.Element == NewKey { + let lock: UUID + let keys: Keys let newKeys: NewKeys @@ -245,7 +248,7 @@ internal extension PermissionsView { private extension PermissionsView.StateView { func row(for item: Key) -> some View { - AppNavigationLink(id: .key(.key(item)), label: { + AppNavigationLink(id: .key(lock, .key(item)), label: { LockRowView( image: .permission(item.permission.type), title: item.name, @@ -259,7 +262,7 @@ private extension PermissionsView.StateView { } func row(for item: NewKey, invitationURL: URL?) -> some View { - AppNavigationLink(id: .key(.newKey(item)), label: { + AppNavigationLink(id: .key(lock, .newKey(item)), label: { HStack(alignment: .center, spacing: 8) { row(for: item, showDate: invitationURL == nil) if let url = invitationURL { @@ -270,6 +273,10 @@ private extension PermissionsView.StateView { item: url, subject: Text("\(item.name)"), message: Text("Share this key"), + preview: SharePreview( + item.name, + icon: Image(permissionType: item.permission.type) + ), label: { shareImage } ) .buttonStyle(.plain) @@ -303,14 +310,6 @@ private extension PermissionsView.StateView { .padding(8) } - func destination(for item: Key) -> some View { - KeyDetailView(key: .key(item)) - } - - func destination(for item: NewKey) -> some View { - KeyDetailView(key: .newKey(item)) - } - func deleteKey(at indexSet: IndexSet) { } @@ -328,6 +327,7 @@ struct PermissionsView_Previews: PreviewProvider { static var previews: some View { NavigationView { PermissionsView.StateView( + lock: UUID(), keys: [ Key( id: UUID(), diff --git a/Xcode/SmartLock/View/KeysView.swift b/Xcode/SmartLock/View/KeysView.swift index cc30a30e..266b9d26 100644 --- a/Xcode/SmartLock/View/KeysView.swift +++ b/Xcode/SmartLock/View/KeysView.swift @@ -132,7 +132,7 @@ private extension KeysView.StateView { ) ) case let .invitation(invitation, lockName): - return AnyView(AppNavigationLink(id: .key(.newKey(invitation.key))) { // FIXME: Show invitation view + return AnyView(AppNavigationLink(id: .key(invitation.lock, .newKey(invitation.key))) { // FIXME: Show invitation view LockRowView( image: .permission(invitation.key.permission.type), title: invitation.key.name, diff --git a/Xcode/SmartLock/View/SidebarView.swift b/Xcode/SmartLock/View/SidebarView.swift index d46c5ba1..d0cab52e 100644 --- a/Xcode/SmartLock/View/SidebarView.swift +++ b/Xcode/SmartLock/View/SidebarView.swift @@ -115,7 +115,6 @@ private extension SidebarView { isNearbyExpanded = newValue // start scanning if not already if newValue, !store.isScanning { - store.peripherals.removeAll(keepingCapacity: true) Task { try? await Task.sleep(nanoseconds: 1_000_000_000) store.scanDefault() From bd86803a3841f0f5b4d40e4c4e6ada4ee7690727 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 26 Sep 2022 00:00:28 -0700 Subject: [PATCH 176/229] [App] Updated Info.plist --- Xcode/SmartLock.xcodeproj/project.pbxproj | 12 ++++ Xcode/SmartLock/Info.plist | 73 +++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index b0b73013..7476e068 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -1214,9 +1214,14 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SmartLock/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Cerradura; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bluetooth is used to scan for nearby locks in the background."; INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Bluetooth is needed to connect to your smart lock."; + INFOPLIST_KEY_NSCameraUsageDescription = "Your camera is needed to setup your smart lock."; + INFOPLIST_KEY_NSContactsUsageDescription = "Access to your contacts is needed to share keys with friends."; INFOPLIST_KEY_NSFaceIDUsageDescription = "Your FaceID is needed to unlock."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Your location is needed to find nearby smart locks."; + INFOPLIST_KEY_NSSiriUsageDescription = "Siri is used to unlock with voice commands."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -1227,6 +1232,7 @@ "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportsDocumentBrowser = YES; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock; @@ -1259,9 +1265,14 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SmartLock/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Cerradura; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bluetooth is used to scan for nearby locks in the background."; INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Bluetooth is needed to connect to your smart lock."; + INFOPLIST_KEY_NSCameraUsageDescription = "Your camera is needed to setup your smart lock."; + INFOPLIST_KEY_NSContactsUsageDescription = "Access to your contacts is needed to share keys with friends."; INFOPLIST_KEY_NSFaceIDUsageDescription = "Your FaceID is needed to unlock."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Your location is needed to find nearby smart locks."; + INFOPLIST_KEY_NSSiriUsageDescription = "Siri is used to unlock with voice commands."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -1272,6 +1283,7 @@ "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportsDocumentBrowser = YES; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock; diff --git a/Xcode/SmartLock/Info.plist b/Xcode/SmartLock/Info.plist index aff95fb9..ff5ed891 100644 --- a/Xcode/SmartLock/Info.plist +++ b/Xcode/SmartLock/Info.plist @@ -2,6 +2,50 @@ + CFBundleDocumentTypes + + + CFBundleTypeName + Key + CFBundleTypeRole + Editor + LSHandlerRank + Owner + LSItemContentTypes + + com.colemancda.lock.key + + + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + com.colemancda.Lock + CFBundleURLSchemes + + lock + + + + CKSharingSupported + + ITSAppUsesNonExemptEncryption + + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + NSUserActivityTypes + + UnlockIntent + com.colemancda.lock.activity.action + com.colemancda.lock.activity.screen + com.colemancda.lock.activity.view + UIBackgroundModes bluetooth-central @@ -10,5 +54,34 @@ processing remote-notification + UIFileSharingEnabled + + UTExportedTypeDeclarations + + + UTTypeConformsTo + + public.data + public.content + + UTTypeDescription + Key File + UTTypeIcons + + UTTypeIdentifier + com.colemancda.lock.key + UTTypeTagSpecification + + public.filename-extension + + ekey + + public.mime-type + + lock/x-ekey + + + + From d9fc4586014c1b05a067a718bcb68f30875e2c20 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 26 Sep 2022 10:12:30 -0700 Subject: [PATCH 177/229] [App] Added iOS QuickLook extension --- .../Base.lproj/MainInterface.storyboard | 45 ++++ Xcode/LockQuickLook-iOS/Info.plist | 24 ++ .../LockQuickLook-iOS.entitlements | 26 +++ Xcode/LockQuickLook-iOS/PreviewProvider.swift | 57 +++++ .../PreviewViewController.swift | 91 ++++++++ Xcode/SmartLock.xcodeproj/project.pbxproj | 221 ++++++++++++++++-- .../xcschemes/LockQuickLook.xcscheme | 95 ++++++++ 7 files changed, 546 insertions(+), 13 deletions(-) create mode 100644 Xcode/LockQuickLook-iOS/Base.lproj/MainInterface.storyboard create mode 100644 Xcode/LockQuickLook-iOS/Info.plist create mode 100644 Xcode/LockQuickLook-iOS/LockQuickLook-iOS.entitlements create mode 100644 Xcode/LockQuickLook-iOS/PreviewProvider.swift create mode 100644 Xcode/LockQuickLook-iOS/PreviewViewController.swift create mode 100644 Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockQuickLook.xcscheme diff --git a/Xcode/LockQuickLook-iOS/Base.lproj/MainInterface.storyboard b/Xcode/LockQuickLook-iOS/Base.lproj/MainInterface.storyboard new file mode 100644 index 00000000..ebc86703 --- /dev/null +++ b/Xcode/LockQuickLook-iOS/Base.lproj/MainInterface.storyboard @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Xcode/LockQuickLook-iOS/Info.plist b/Xcode/LockQuickLook-iOS/Info.plist new file mode 100644 index 00000000..5e3360d6 --- /dev/null +++ b/Xcode/LockQuickLook-iOS/Info.plist @@ -0,0 +1,24 @@ + + + + + NSExtension + + NSExtensionAttributes + + QLIsDataBasedPreview + + QLSupportedContentTypes + + com.colemancda.lock.key + + QLSupportsSearchableItems + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.quicklook.preview + + + diff --git a/Xcode/LockQuickLook-iOS/LockQuickLook-iOS.entitlements b/Xcode/LockQuickLook-iOS/LockQuickLook-iOS.entitlements new file mode 100644 index 00000000..e23402c4 --- /dev/null +++ b/Xcode/LockQuickLook-iOS/LockQuickLook-iOS.entitlements @@ -0,0 +1,26 @@ + + + + + aps-environment + development + com.apple.developer.icloud-container-identifiers + + iCloud.com.colemancda.Lock + + com.apple.developer.icloud-services + + CloudKit + + com.apple.developer.ubiquity-kvstore-identifier + $(TeamIdentifierPrefix)$(CFBundleIdentifier) + com.apple.security.application-groups + + group.com.colemancda.Lock + + keychain-access-groups + + $(AppIdentifierPrefix)com.colemancda.Lock + + + diff --git a/Xcode/LockQuickLook-iOS/PreviewProvider.swift b/Xcode/LockQuickLook-iOS/PreviewProvider.swift new file mode 100644 index 00000000..693d0a2b --- /dev/null +++ b/Xcode/LockQuickLook-iOS/PreviewProvider.swift @@ -0,0 +1,57 @@ +// +// PreviewProvider.swift +// LockQuickLook +// +// Created by Alsey Coleman Miller on 9/26/22. +// + +import QuickLook + +class PreviewProvider: QLPreviewProvider, QLPreviewingController { + + + /* + Use a QLPreviewProvider to provide data-based previews. + + To set up your extension as a data-based preview extension: + + - Modify the extension's Info.plist by setting + QLIsDataBasedPreview + + + - Add the supported content types to QLSupportedContentTypes array in the extension's Info.plist. + + - Remove + NSExtensionMainStoryboard + MainInterface + + and replace it by setting the NSExtensionPrincipalClass to this class, e.g. + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).PreviewProvider + + - Implement providePreview(for:) + */ + + func providePreview(for request: QLFilePreviewRequest) async throws -> QLPreviewReply { + + //You can create a QLPreviewReply in several ways, depending on the format of the data you want to return. + //To return Data of a supported content type: + + let contentType = UTType.plainText // replace with your data type + + let reply = QLPreviewReply.init(dataOfContentType: contentType, contentSize: CGSize.init(width: 800, height: 800)) { (replyToUpdate : QLPreviewReply) in + + let data = Data("Test Data".utf8) + + //setting the stringEncoding for text and html data is optional and defaults to String.Encoding.utf8 + replyToUpdate.stringEncoding = .utf8 + + //initialize your data here + + return data + } + + return reply + } + +} diff --git a/Xcode/LockQuickLook-iOS/PreviewViewController.swift b/Xcode/LockQuickLook-iOS/PreviewViewController.swift new file mode 100644 index 00000000..02c31ff8 --- /dev/null +++ b/Xcode/LockQuickLook-iOS/PreviewViewController.swift @@ -0,0 +1,91 @@ +// +// PreviewViewController.swift +// LockQuickLook +// +// Created by Alsey Coleman Miller on 9/26/22. +// + +import Foundation +import UIKit +import QuickLook +import LockKit +import SwiftUI + +final class PreviewViewController: UIViewController, QLPreviewingController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + assert(Thread.isMainThread) + // set global appearance + UIView.configureLockAppearance() + // log + log("👁‍🗨 Loaded \(PreviewViewController.self)") + } + + func preparePreviewOfSearchableItem(identifier: String, queryString: String?) async throws { + log("👁‍🗨 Prepare preview for searchable item \(identifier) \(queryString ?? "")") + + } + + func preparePreviewOfFile(at url: URL) async throws { + + log("👁‍🗨 Prepare preview for \(url)") + + guard url.pathExtension == "ekey" else { + log("⚠️ Not eKey file: \(url)") + throw CocoaError(.fileReadInvalidFileName) + } + + let decodeTask = Task { + let decoder = JSONDecoder() + let data = try Data(contentsOf: url, options: [.mappedIfSafe]) + let invitation = try decoder.decode(NewKey.Invitation.self, from: data) + return invitation + } + + // load store + let _ = Store.shared + let invitation = try await decodeTask.value + + // update UI + await MainActor.run { + loadNewKey(invitation) + } + } +} + +@MainActor +private extension PreviewViewController { + + func loadNewKey(_ invitation: NewKey.Invitation) { + assert(Thread.isMainThread) + let viewController = UIHostingController( + rootView: NewKeyInvitationView(invitation: invitation) + .environmentObject(Store.shared) + ) + loadChildViewController(viewController) + } + + func loadChildViewController(_ viewController: UIViewController) { + assert(Thread.isMainThread) + viewController.loadViewIfNeeded() + viewController.view.layoutIfNeeded() + addChild(viewController) + view.addSubview(viewController.view) + viewController.didMove(toParent: self) + + guard let childView = viewController.view else { + assertionFailure() + return + } + + childView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + childView.leftAnchor.constraint(equalTo: view.leftAnchor), + childView.rightAnchor.constraint(equalTo: view.rightAnchor), + childView.topAnchor.constraint(equalTo: view.topAnchor), + childView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } +} diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 7476e068..25b050c7 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -87,6 +87,12 @@ 6E4CB62128D7907200116573 /* PermissionIconViewUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62028D7907200116573 /* PermissionIconViewUIView.swift */; }; 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62228D7907E00116573 /* PermissionIconViewNSView.swift */; }; 6E4CB62528D792BF00116573 /* InfoPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62428D792BF00116573 /* InfoPlist.swift */; }; + 6E5D51DF28E20C6D008FFB4D /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E5D51C128E185DB008FFB4D /* QuickLook.framework */; }; + 6E5D51E228E20C6D008FFB4D /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D51E128E20C6D008FFB4D /* PreviewViewController.swift */; }; + 6E5D51E428E20C6D008FFB4D /* PreviewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D51E328E20C6D008FFB4D /* PreviewProvider.swift */; }; + 6E5D51E728E20C6D008FFB4D /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6E5D51E528E20C6D008FFB4D /* MainInterface.storyboard */; }; + 6E5D51EB28E20C6D008FFB4D /* LockQuickLook.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6E5D51DE28E20C6C008FFB4D /* LockQuickLook.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 6E5D51F028E20E8E008FFB4D /* LockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; }; 6E6A97EF28DBEC2D00C689F6 /* NewKeyInvitationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */; }; 6E6A97F328DC030000C689F6 /* NewKeyInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */; }; 6E84E52728DDC841008CAE85 /* ScanLocksIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFEC28DD301B00F03735 /* ScanLocksIntent.swift */; }; @@ -128,6 +134,20 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 6E5D51E928E20C6D008FFB4D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6EA7767928D7061600018FA3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6E5D51DD28E20C6C008FFB4D; + remoteInfo = LockQuickLook; + }; + 6E5D51F228E20E8E008FFB4D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6EA7767928D7061600018FA3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6EA7769C28D707FE00018FA3; + remoteInfo = LockKit; + }; 6E8BBFF828DD30B400F03735 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 6EA7767928D7061600018FA3 /* Project object */; @@ -145,6 +165,17 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 6E5D51D228E185DB008FFB4D /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 6E5D51EB28E20C6D008FFB4D /* LockQuickLook.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; 6EA776A828D707FE00018FA3 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -237,6 +268,13 @@ 6E4CB62028D7907200116573 /* PermissionIconViewUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionIconViewUIView.swift; sourceTree = ""; }; 6E4CB62228D7907E00116573 /* PermissionIconViewNSView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionIconViewNSView.swift; sourceTree = ""; }; 6E4CB62428D792BF00116573 /* InfoPlist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InfoPlist.swift; path = ../../../iOS/SmartLock/InfoPlist.swift; sourceTree = ""; }; + 6E5D51C128E185DB008FFB4D /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = System/Library/Frameworks/QuickLook.framework; sourceTree = SDKROOT; }; + 6E5D51DE28E20C6C008FFB4D /* LockQuickLook.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LockQuickLook.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 6E5D51E128E20C6D008FFB4D /* PreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewViewController.swift; sourceTree = ""; }; + 6E5D51E328E20C6D008FFB4D /* PreviewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewProvider.swift; sourceTree = ""; }; + 6E5D51E628E20C6D008FFB4D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + 6E5D51E828E20C6D008FFB4D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 6E5D51EF28E20CAC008FFB4D /* LockQuickLook-iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "LockQuickLook-iOS.entitlements"; sourceTree = ""; }; 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyInvitationStore.swift; sourceTree = ""; }; 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyInvitationView.swift; sourceTree = ""; }; 6E84E51F28DD9646008CAE85 /* Shortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shortcuts.swift; sourceTree = ""; }; @@ -288,6 +326,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6E5D51DB28E20C6C008FFB4D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E5D51F028E20E8E008FFB4D /* LockKit.framework in Frameworks */, + 6E5D51DF28E20C6D008FFB4D /* QuickLook.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6E8BBFE528DD301B00F03735 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -484,6 +531,18 @@ path = AppKit; sourceTree = ""; }; + 6E5D51E028E20C6D008FFB4D /* LockQuickLook-iOS */ = { + isa = PBXGroup; + children = ( + 6E5D51EF28E20CAC008FFB4D /* LockQuickLook-iOS.entitlements */, + 6E5D51E128E20C6D008FFB4D /* PreviewViewController.swift */, + 6E5D51E328E20C6D008FFB4D /* PreviewProvider.swift */, + 6E5D51E528E20C6D008FFB4D /* MainInterface.storyboard */, + 6E5D51E828E20C6D008FFB4D /* Info.plist */, + ); + path = "LockQuickLook-iOS"; + sourceTree = ""; + }; 6E84E53A28DE862B008CAE85 /* AppEntity */ = { isa = PBXGroup; children = ( @@ -562,6 +621,7 @@ 6EA7769E28D707FE00018FA3 /* LockKit */, 6E3276C428D70A3700AF171B /* MatterLock */, 6E8BBFE928DD301B00F03735 /* LockIntents */, + 6E5D51E028E20C6D008FFB4D /* LockQuickLook-iOS */, 6EA7768228D7061600018FA3 /* Products */, 6EA776A928D7082300018FA3 /* Frameworks */, ); @@ -574,6 +634,7 @@ 6EA7769D28D707FE00018FA3 /* LockKit.framework */, 6E3276C128D70A3700AF171B /* MatterLock.appex */, 6E8BBFE828DD301B00F03735 /* LockIntents.appex */, + 6E5D51DE28E20C6C008FFB4D /* LockQuickLook.appex */, ); name = Products; sourceTree = ""; @@ -617,6 +678,7 @@ isa = PBXGroup; children = ( 6E3276C228D70A3700AF171B /* HomeKit.framework */, + 6E5D51C128E185DB008FFB4D /* QuickLook.framework */, ); name = Frameworks; sourceTree = ""; @@ -652,6 +714,24 @@ productReference = 6E3276C128D70A3700AF171B /* MatterLock.appex */; productType = "com.apple.product-type.app-extension"; }; + 6E5D51DD28E20C6C008FFB4D /* LockQuickLook-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6E5D51EC28E20C6D008FFB4D /* Build configuration list for PBXNativeTarget "LockQuickLook-iOS" */; + buildPhases = ( + 6E5D51DA28E20C6C008FFB4D /* Sources */, + 6E5D51DB28E20C6C008FFB4D /* Frameworks */, + 6E5D51DC28E20C6C008FFB4D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 6E5D51F328E20E8E008FFB4D /* PBXTargetDependency */, + ); + name = "LockQuickLook-iOS"; + productName = LockQuickLook; + productReference = 6E5D51DE28E20C6C008FFB4D /* LockQuickLook.appex */; + productType = "com.apple.product-type.app-extension"; + }; 6E8BBFE728DD301B00F03735 /* LockIntents */ = { isa = PBXNativeTarget; buildConfigurationList = 6E8BBFF228DD301B00F03735 /* Build configuration list for PBXNativeTarget "LockIntents" */; @@ -678,11 +758,13 @@ 6EA7767E28D7061600018FA3 /* Frameworks */, 6EA7767F28D7061600018FA3 /* Resources */, 6EA776A828D707FE00018FA3 /* Embed Frameworks */, + 6E5D51D228E185DB008FFB4D /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( 6EA776A228D707FE00018FA3 /* PBXTargetDependency */, + 6E5D51EA28E20C6D008FFB4D /* PBXTargetDependency */, ); name = SmartLock; productName = SmartLock; @@ -729,6 +811,9 @@ 6E3276C028D70A3700AF171B = { CreatedOnToolsVersion = 14.0; }; + 6E5D51DD28E20C6C008FFB4D = { + CreatedOnToolsVersion = 14.1; + }; 6E8BBFE728DD301B00F03735 = { CreatedOnToolsVersion = 14.1; }; @@ -765,6 +850,7 @@ 6EA7769C28D707FE00018FA3 /* LockKit */, 6E3276C028D70A3700AF171B /* MatterLock */, 6E8BBFE728DD301B00F03735 /* LockIntents */, + 6E5D51DD28E20C6C008FFB4D /* LockQuickLook-iOS */, ); }; /* End PBXProject section */ @@ -777,6 +863,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6E5D51DC28E20C6C008FFB4D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E5D51E728E20C6D008FFB4D /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6E8BBFE628DD301B00F03735 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -812,6 +906,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6E5D51DA28E20C6C008FFB4D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E5D51E228E20C6D008FFB4D /* PreviewViewController.swift in Sources */, + 6E5D51E428E20C6D008FFB4D /* PreviewProvider.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6E8BBFE428DD301B00F03735 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -938,6 +1041,17 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 6E5D51EA28E20C6D008FFB4D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = 6E5D51DD28E20C6C008FFB4D /* LockQuickLook-iOS */; + targetProxy = 6E5D51E928E20C6D008FFB4D /* PBXContainerItemProxy */; + }; + 6E5D51F328E20E8E008FFB4D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6EA7769C28D707FE00018FA3 /* LockKit */; + targetProxy = 6E5D51F228E20E8E008FFB4D /* PBXContainerItemProxy */; + }; 6E8BBFF928DD30B400F03735 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 6EA7769C28D707FE00018FA3 /* LockKit */; @@ -950,6 +1064,17 @@ }; /* End PBXTargetDependency section */ +/* Begin PBXVariantGroup section */ + 6E5D51E528E20C6D008FFB4D /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 6E5D51E628E20C6D008FFB4D /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ 6E3276CC28D70A3700AF171B /* Debug */ = { isa = XCBuildConfiguration; @@ -966,12 +1091,11 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock.MatterLock; + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.MatterLock; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -991,11 +1115,77 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock.MatterLock; + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.MatterLock; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 6E5D51ED28E20C6D008FFB4D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "LockQuickLook-iOS/LockQuickLook-iOS.entitlements"; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4W79SG34MW; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "LockQuickLook-iOS/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = LockQuickLook; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + "@executable_path/../../../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.QuickLook; + PRODUCT_NAME = LockQuickLook; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6E5D51EE28E20C6D008FFB4D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "LockQuickLook-iOS/LockQuickLook-iOS.entitlements"; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4W79SG34MW; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "LockQuickLook-iOS/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = LockQuickLook; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + "@executable_path/../../../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.QuickLook; + PRODUCT_NAME = LockQuickLook; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; @@ -1025,14 +1215,13 @@ "@executable_path/../../../Frameworks", "@executable_path/../../../../Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock.LockIntents; + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.Intents; PRODUCT_NAME = LockIntents; SDKROOT = auto; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -1060,14 +1249,13 @@ "@executable_path/../../../Frameworks", "@executable_path/../../../../Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock.LockIntents; + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.Intents; PRODUCT_NAME = LockIntents; SDKROOT = auto; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -1133,6 +1321,7 @@ ONLY_ACTIVE_ARCH = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 15.0; WATCHOS_DEPLOYMENT_TARGET = 8.0; }; @@ -1191,6 +1380,7 @@ MTL_FAST_MATH = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 15.0; WATCHOS_DEPLOYMENT_TARGET = 8.0; }; @@ -1235,14 +1425,13 @@ INFOPLIST_KEY_UISupportsDocumentBrowser = YES; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock; + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -1286,14 +1475,13 @@ INFOPLIST_KEY_UISupportsDocumentBrowser = YES; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock; + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -1328,7 +1516,6 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -1364,7 +1551,6 @@ SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -1383,6 +1569,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 6E5D51EC28E20C6D008FFB4D /* Build configuration list for PBXNativeTarget "LockQuickLook-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6E5D51ED28E20C6D008FFB4D /* Debug */, + 6E5D51EE28E20C6D008FFB4D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 6E8BBFF228DD301B00F03735 /* Build configuration list for PBXNativeTarget "LockIntents" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockQuickLook.xcscheme b/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockQuickLook.xcscheme new file mode 100644 index 00000000..15f2bc71 --- /dev/null +++ b/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockQuickLook.xcscheme @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 3b1882bc6ffadf418e98c3f9b345f61d004368f5 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 26 Sep 2022 10:28:43 -0700 Subject: [PATCH 178/229] [App] Added macOS QuickLook extension --- Xcode/LockQuickLook-iOS/PreviewProvider.swift | 57 ------ .../PreviewViewController.swift | 2 - .../Base.lproj/PreviewViewController.xib | 21 ++ Xcode/LockQuickLook-macOS/Info.plist | 22 +++ .../LockQuickLook-macOS.entitlements | 32 +++ .../PreviewViewController.swift | 83 ++++++++ Xcode/SmartLock.xcodeproj/project.pbxproj | 185 +++++++++++++++++- ...ok.xcscheme => LockQuickLook-iOS.xcscheme} | 4 +- 8 files changed, 337 insertions(+), 69 deletions(-) delete mode 100644 Xcode/LockQuickLook-iOS/PreviewProvider.swift create mode 100644 Xcode/LockQuickLook-macOS/Base.lproj/PreviewViewController.xib create mode 100644 Xcode/LockQuickLook-macOS/Info.plist create mode 100644 Xcode/LockQuickLook-macOS/LockQuickLook-macOS.entitlements create mode 100644 Xcode/LockQuickLook-macOS/PreviewViewController.swift rename Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/{LockQuickLook.xcscheme => LockQuickLook-iOS.xcscheme} (96%) diff --git a/Xcode/LockQuickLook-iOS/PreviewProvider.swift b/Xcode/LockQuickLook-iOS/PreviewProvider.swift deleted file mode 100644 index 693d0a2b..00000000 --- a/Xcode/LockQuickLook-iOS/PreviewProvider.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// PreviewProvider.swift -// LockQuickLook -// -// Created by Alsey Coleman Miller on 9/26/22. -// - -import QuickLook - -class PreviewProvider: QLPreviewProvider, QLPreviewingController { - - - /* - Use a QLPreviewProvider to provide data-based previews. - - To set up your extension as a data-based preview extension: - - - Modify the extension's Info.plist by setting - QLIsDataBasedPreview - - - - Add the supported content types to QLSupportedContentTypes array in the extension's Info.plist. - - - Remove - NSExtensionMainStoryboard - MainInterface - - and replace it by setting the NSExtensionPrincipalClass to this class, e.g. - NSExtensionPrincipalClass - $(PRODUCT_MODULE_NAME).PreviewProvider - - - Implement providePreview(for:) - */ - - func providePreview(for request: QLFilePreviewRequest) async throws -> QLPreviewReply { - - //You can create a QLPreviewReply in several ways, depending on the format of the data you want to return. - //To return Data of a supported content type: - - let contentType = UTType.plainText // replace with your data type - - let reply = QLPreviewReply.init(dataOfContentType: contentType, contentSize: CGSize.init(width: 800, height: 800)) { (replyToUpdate : QLPreviewReply) in - - let data = Data("Test Data".utf8) - - //setting the stringEncoding for text and html data is optional and defaults to String.Encoding.utf8 - replyToUpdate.stringEncoding = .utf8 - - //initialize your data here - - return data - } - - return reply - } - -} diff --git a/Xcode/LockQuickLook-iOS/PreviewViewController.swift b/Xcode/LockQuickLook-iOS/PreviewViewController.swift index 02c31ff8..c53cb564 100644 --- a/Xcode/LockQuickLook-iOS/PreviewViewController.swift +++ b/Xcode/LockQuickLook-iOS/PreviewViewController.swift @@ -17,8 +17,6 @@ final class PreviewViewController: UIViewController, QLPreviewingController { super.viewDidLoad() // Do any additional setup after loading the view. assert(Thread.isMainThread) - // set global appearance - UIView.configureLockAppearance() // log log("👁‍🗨 Loaded \(PreviewViewController.self)") } diff --git a/Xcode/LockQuickLook-macOS/Base.lproj/PreviewViewController.xib b/Xcode/LockQuickLook-macOS/Base.lproj/PreviewViewController.xib new file mode 100644 index 00000000..36f5ffc3 --- /dev/null +++ b/Xcode/LockQuickLook-macOS/Base.lproj/PreviewViewController.xib @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Xcode/LockQuickLook-macOS/Info.plist b/Xcode/LockQuickLook-macOS/Info.plist new file mode 100644 index 00000000..648990fe --- /dev/null +++ b/Xcode/LockQuickLook-macOS/Info.plist @@ -0,0 +1,22 @@ + + + + + NSExtension + + NSExtensionAttributes + + QLIsDataBasedPreview + + QLSupportedContentTypes + + QLSupportsSearchableItems + + + NSExtensionPointIdentifier + com.apple.quicklook.preview + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).PreviewViewController + + + diff --git a/Xcode/LockQuickLook-macOS/LockQuickLook-macOS.entitlements b/Xcode/LockQuickLook-macOS/LockQuickLook-macOS.entitlements new file mode 100644 index 00000000..48e803ac --- /dev/null +++ b/Xcode/LockQuickLook-macOS/LockQuickLook-macOS.entitlements @@ -0,0 +1,32 @@ + + + + + com.apple.developer.aps-environment + development + com.apple.developer.icloud-container-identifiers + + iCloud.com.colemancda.Lock + + com.apple.developer.icloud-services + + CloudKit + + com.apple.developer.ubiquity-kvstore-identifier + $(TeamIdentifierPrefix)$(CFBundleIdentifier) + com.apple.security.app-sandbox + + com.apple.security.application-groups + + $(TeamIdentifierPrefix).group.com.colemancda.lock + + com.apple.security.device.bluetooth + + com.apple.security.files.user-selected.read-only + + keychain-access-groups + + $(AppIdentifierPrefix)com.colemancda.Lock + + + diff --git a/Xcode/LockQuickLook-macOS/PreviewViewController.swift b/Xcode/LockQuickLook-macOS/PreviewViewController.swift new file mode 100644 index 00000000..ffaba7ef --- /dev/null +++ b/Xcode/LockQuickLook-macOS/PreviewViewController.swift @@ -0,0 +1,83 @@ +// +// PreviewViewController.swift +// LockQuickLook-macOS +// +// Created by Alsey Coleman Miller on 9/26/22. +// + +import Cocoa +import Quartz +import SwiftUI +import LockKit + +final class PreviewViewController: NSViewController, QLPreviewingController { + + override var nibName: NSNib.Name? { + return NSNib.Name("PreviewViewController") + } + + override func loadView() { + super.loadView() + // Do any additional setup after loading the view. + assert(Thread.isMainThread) + // log + log("👁‍🗨 Loaded \(PreviewViewController.self)") + } + + func preparePreviewOfFile(at url: URL) async throws { + + log("👁‍🗨 Prepare preview for \(url)") + + guard url.pathExtension == "ekey" else { + log("⚠️ Not eKey file: \(url)") + throw CocoaError(.fileReadInvalidFileName) + } + + let decodeTask = Task { + let decoder = JSONDecoder() + let data = try Data(contentsOf: url, options: [.mappedIfSafe]) + let invitation = try decoder.decode(NewKey.Invitation.self, from: data) + return invitation + } + + // load store + let _ = Store.shared + let invitation = try await decodeTask.value + + // update UI + await MainActor.run { + loadNewKey(invitation) + } + } +} + +@MainActor +private extension PreviewViewController { + + func loadNewKey(_ invitation: NewKey.Invitation) { + assert(Thread.isMainThread) + let viewController = NSHostingController( + rootView: NewKeyInvitationView(invitation: invitation) + .environmentObject(Store.shared) + ) + loadChildViewController(viewController) + } + + func loadChildViewController(_ viewController: NSViewController) { + assert(Thread.isMainThread) + viewController.loadView() + viewController.view.layout() + addChild(viewController) + view.addSubview(viewController.view) + //viewController .didMove(toParent: self) + let childView = viewController.view + childView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + childView.leftAnchor.constraint(equalTo: view.leftAnchor), + childView.rightAnchor.constraint(equalTo: view.rightAnchor), + childView.topAnchor.constraint(equalTo: view.topAnchor), + childView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } +} + diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 25b050c7..09a4e9b1 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -89,10 +89,14 @@ 6E4CB62528D792BF00116573 /* InfoPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62428D792BF00116573 /* InfoPlist.swift */; }; 6E5D51DF28E20C6D008FFB4D /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E5D51C128E185DB008FFB4D /* QuickLook.framework */; }; 6E5D51E228E20C6D008FFB4D /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D51E128E20C6D008FFB4D /* PreviewViewController.swift */; }; - 6E5D51E428E20C6D008FFB4D /* PreviewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D51E328E20C6D008FFB4D /* PreviewProvider.swift */; }; 6E5D51E728E20C6D008FFB4D /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6E5D51E528E20C6D008FFB4D /* MainInterface.storyboard */; }; 6E5D51EB28E20C6D008FFB4D /* LockQuickLook.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6E5D51DE28E20C6C008FFB4D /* LockQuickLook.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 6E5D51F028E20E8E008FFB4D /* LockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; }; + 6E5D51FB28E21538008FFB4D /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E5D51FA28E21537008FFB4D /* Quartz.framework */; }; + 6E5D51FE28E21538008FFB4D /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D51FD28E21538008FFB4D /* PreviewViewController.swift */; }; + 6E5D520328E21538008FFB4D /* PreviewViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6E5D520128E21538008FFB4D /* PreviewViewController.xib */; }; + 6E5D520828E21538008FFB4D /* LockQuickLook.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6E5D51F928E21537008FFB4D /* LockQuickLook.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 6E5D520C28E215E9008FFB4D /* LockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; }; 6E6A97EF28DBEC2D00C689F6 /* NewKeyInvitationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */; }; 6E6A97F328DC030000C689F6 /* NewKeyInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */; }; 6E84E52728DDC841008CAE85 /* ScanLocksIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFEC28DD301B00F03735 /* ScanLocksIntent.swift */; }; @@ -148,6 +152,20 @@ remoteGlobalIDString = 6EA7769C28D707FE00018FA3; remoteInfo = LockKit; }; + 6E5D520628E21538008FFB4D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6EA7767928D7061600018FA3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6E5D51F828E21537008FFB4D; + remoteInfo = "LockQuickLook-macOS"; + }; + 6E5D520E28E215E9008FFB4D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6EA7767928D7061600018FA3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6EA7769C28D707FE00018FA3; + remoteInfo = LockKit; + }; 6E8BBFF828DD30B400F03735 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 6EA7767928D7061600018FA3 /* Project object */; @@ -171,6 +189,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( + 6E5D520828E21538008FFB4D /* LockQuickLook.appex in Embed Foundation Extensions */, 6E5D51EB28E20C6D008FFB4D /* LockQuickLook.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; @@ -271,10 +290,15 @@ 6E5D51C128E185DB008FFB4D /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = System/Library/Frameworks/QuickLook.framework; sourceTree = SDKROOT; }; 6E5D51DE28E20C6C008FFB4D /* LockQuickLook.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LockQuickLook.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6E5D51E128E20C6D008FFB4D /* PreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewViewController.swift; sourceTree = ""; }; - 6E5D51E328E20C6D008FFB4D /* PreviewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewProvider.swift; sourceTree = ""; }; 6E5D51E628E20C6D008FFB4D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; 6E5D51E828E20C6D008FFB4D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 6E5D51EF28E20CAC008FFB4D /* LockQuickLook-iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "LockQuickLook-iOS.entitlements"; sourceTree = ""; }; + 6E5D51F928E21537008FFB4D /* LockQuickLook.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LockQuickLook.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 6E5D51FA28E21537008FFB4D /* Quartz.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quartz.framework; path = System/Library/Frameworks/Quartz.framework; sourceTree = SDKROOT; }; + 6E5D51FD28E21538008FFB4D /* PreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewViewController.swift; sourceTree = ""; }; + 6E5D520228E21538008FFB4D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/PreviewViewController.xib; sourceTree = ""; }; + 6E5D520428E21538008FFB4D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 6E5D520528E21538008FFB4D /* LockQuickLook-macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "LockQuickLook-macOS.entitlements"; sourceTree = ""; }; 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyInvitationStore.swift; sourceTree = ""; }; 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyInvitationView.swift; sourceTree = ""; }; 6E84E51F28DD9646008CAE85 /* Shortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shortcuts.swift; sourceTree = ""; }; @@ -335,6 +359,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6E5D51F628E21537008FFB4D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E5D520C28E215E9008FFB4D /* LockKit.framework in Frameworks */, + 6E5D51FB28E21538008FFB4D /* Quartz.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6E8BBFE528DD301B00F03735 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -534,15 +567,25 @@ 6E5D51E028E20C6D008FFB4D /* LockQuickLook-iOS */ = { isa = PBXGroup; children = ( + 6E5D51E828E20C6D008FFB4D /* Info.plist */, 6E5D51EF28E20CAC008FFB4D /* LockQuickLook-iOS.entitlements */, - 6E5D51E128E20C6D008FFB4D /* PreviewViewController.swift */, - 6E5D51E328E20C6D008FFB4D /* PreviewProvider.swift */, 6E5D51E528E20C6D008FFB4D /* MainInterface.storyboard */, - 6E5D51E828E20C6D008FFB4D /* Info.plist */, + 6E5D51E128E20C6D008FFB4D /* PreviewViewController.swift */, ); path = "LockQuickLook-iOS"; sourceTree = ""; }; + 6E5D51FC28E21538008FFB4D /* LockQuickLook-macOS */ = { + isa = PBXGroup; + children = ( + 6E5D520428E21538008FFB4D /* Info.plist */, + 6E5D520528E21538008FFB4D /* LockQuickLook-macOS.entitlements */, + 6E5D51FD28E21538008FFB4D /* PreviewViewController.swift */, + 6E5D520128E21538008FFB4D /* PreviewViewController.xib */, + ); + path = "LockQuickLook-macOS"; + sourceTree = ""; + }; 6E84E53A28DE862B008CAE85 /* AppEntity */ = { isa = PBXGroup; children = ( @@ -622,6 +665,7 @@ 6E3276C428D70A3700AF171B /* MatterLock */, 6E8BBFE928DD301B00F03735 /* LockIntents */, 6E5D51E028E20C6D008FFB4D /* LockQuickLook-iOS */, + 6E5D51FC28E21538008FFB4D /* LockQuickLook-macOS */, 6EA7768228D7061600018FA3 /* Products */, 6EA776A928D7082300018FA3 /* Frameworks */, ); @@ -635,6 +679,7 @@ 6E3276C128D70A3700AF171B /* MatterLock.appex */, 6E8BBFE828DD301B00F03735 /* LockIntents.appex */, 6E5D51DE28E20C6C008FFB4D /* LockQuickLook.appex */, + 6E5D51F928E21537008FFB4D /* LockQuickLook.appex */, ); name = Products; sourceTree = ""; @@ -679,6 +724,7 @@ children = ( 6E3276C228D70A3700AF171B /* HomeKit.framework */, 6E5D51C128E185DB008FFB4D /* QuickLook.framework */, + 6E5D51FA28E21537008FFB4D /* Quartz.framework */, ); name = Frameworks; sourceTree = ""; @@ -732,6 +778,24 @@ productReference = 6E5D51DE28E20C6C008FFB4D /* LockQuickLook.appex */; productType = "com.apple.product-type.app-extension"; }; + 6E5D51F828E21537008FFB4D /* LockQuickLook-macOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6E5D520928E21538008FFB4D /* Build configuration list for PBXNativeTarget "LockQuickLook-macOS" */; + buildPhases = ( + 6E5D51F528E21537008FFB4D /* Sources */, + 6E5D51F628E21537008FFB4D /* Frameworks */, + 6E5D51F728E21537008FFB4D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 6E5D520F28E215E9008FFB4D /* PBXTargetDependency */, + ); + name = "LockQuickLook-macOS"; + productName = "LockQuickLook-macOS"; + productReference = 6E5D51F928E21537008FFB4D /* LockQuickLook.appex */; + productType = "com.apple.product-type.app-extension"; + }; 6E8BBFE728DD301B00F03735 /* LockIntents */ = { isa = PBXNativeTarget; buildConfigurationList = 6E8BBFF228DD301B00F03735 /* Build configuration list for PBXNativeTarget "LockIntents" */; @@ -765,6 +829,7 @@ dependencies = ( 6EA776A228D707FE00018FA3 /* PBXTargetDependency */, 6E5D51EA28E20C6D008FFB4D /* PBXTargetDependency */, + 6E5D520728E21538008FFB4D /* PBXTargetDependency */, ); name = SmartLock; productName = SmartLock; @@ -814,6 +879,9 @@ 6E5D51DD28E20C6C008FFB4D = { CreatedOnToolsVersion = 14.1; }; + 6E5D51F828E21537008FFB4D = { + CreatedOnToolsVersion = 14.1; + }; 6E8BBFE728DD301B00F03735 = { CreatedOnToolsVersion = 14.1; }; @@ -851,6 +919,7 @@ 6E3276C028D70A3700AF171B /* MatterLock */, 6E8BBFE728DD301B00F03735 /* LockIntents */, 6E5D51DD28E20C6C008FFB4D /* LockQuickLook-iOS */, + 6E5D51F828E21537008FFB4D /* LockQuickLook-macOS */, ); }; /* End PBXProject section */ @@ -871,6 +940,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6E5D51F728E21537008FFB4D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E5D520328E21538008FFB4D /* PreviewViewController.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6E8BBFE628DD301B00F03735 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -911,7 +988,14 @@ buildActionMask = 2147483647; files = ( 6E5D51E228E20C6D008FFB4D /* PreviewViewController.swift in Sources */, - 6E5D51E428E20C6D008FFB4D /* PreviewProvider.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6E5D51F528E21537008FFB4D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E5D51FE28E21538008FFB4D /* PreviewViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1052,6 +1136,16 @@ target = 6EA7769C28D707FE00018FA3 /* LockKit */; targetProxy = 6E5D51F228E20E8E008FFB4D /* PBXContainerItemProxy */; }; + 6E5D520728E21538008FFB4D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6E5D51F828E21537008FFB4D /* LockQuickLook-macOS */; + targetProxy = 6E5D520628E21538008FFB4D /* PBXContainerItemProxy */; + }; + 6E5D520F28E215E9008FFB4D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6EA7769C28D707FE00018FA3 /* LockKit */; + targetProxy = 6E5D520E28E215E9008FFB4D /* PBXContainerItemProxy */; + }; 6E8BBFF928DD30B400F03735 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 6EA7769C28D707FE00018FA3 /* LockKit */; @@ -1073,6 +1167,14 @@ name = MainInterface.storyboard; sourceTree = ""; }; + 6E5D520128E21538008FFB4D /* PreviewViewController.xib */ = { + isa = PBXVariantGroup; + children = ( + 6E5D520228E21538008FFB4D /* Base */, + ); + name = PreviewViewController.xib; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -1139,7 +1241,6 @@ INFOPLIST_FILE = "LockQuickLook-iOS/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = LockQuickLook; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1172,7 +1273,6 @@ INFOPLIST_FILE = "LockQuickLook-iOS/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = LockQuickLook; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1192,6 +1292,66 @@ }; name = Release; }; + 6E5D520A28E21538008FFB4D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "LockQuickLook-macOS/LockQuickLook-macOS.entitlements"; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4W79SG34MW; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "LockQuickLook-macOS/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = LockQuickLook; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.QuickLook; + PRODUCT_NAME = LockQuickLook; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 6E5D520B28E21538008FFB4D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "LockQuickLook-macOS/LockQuickLook-macOS.entitlements"; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4W79SG34MW; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "LockQuickLook-macOS/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = LockQuickLook; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.QuickLook; + PRODUCT_NAME = LockQuickLook; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; 6E8BBFF328DD301B00F03735 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1578,6 +1738,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 6E5D520928E21538008FFB4D /* Build configuration list for PBXNativeTarget "LockQuickLook-macOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6E5D520A28E21538008FFB4D /* Debug */, + 6E5D520B28E21538008FFB4D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 6E8BBFF228DD301B00F03735 /* Build configuration list for PBXNativeTarget "LockIntents" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockQuickLook.xcscheme b/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockQuickLook-iOS.xcscheme similarity index 96% rename from Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockQuickLook.xcscheme rename to Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockQuickLook-iOS.xcscheme index 15f2bc71..4a8406c1 100644 --- a/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockQuickLook.xcscheme +++ b/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockQuickLook-iOS.xcscheme @@ -15,8 +15,8 @@ buildForAnalyzing = "YES"> From de12cac20971ec5408d720d5c8804322a4ec9085 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 26 Sep 2022 13:25:25 -0700 Subject: [PATCH 179/229] [App] Added document opening --- Xcode/LockKit/Model/NewKeyDocument.swift | 20 +- Xcode/LockKit/View/NavigationLink.swift | 2 +- Xcode/SmartLock.xcodeproj/project.pbxproj | 29 ++- Xcode/SmartLock/App.swift | 226 +++++++++++++++++++++- Xcode/SmartLock/View/TabBarView.swift | 66 +++++++ 5 files changed, 322 insertions(+), 21 deletions(-) diff --git a/Xcode/LockKit/Model/NewKeyDocument.swift b/Xcode/LockKit/Model/NewKeyDocument.swift index 270fb098..98f6f425 100644 --- a/Xcode/LockKit/Model/NewKeyDocument.swift +++ b/Xcode/LockKit/Model/NewKeyDocument.swift @@ -18,18 +18,32 @@ public extension NewKey.Invitation { /// New Key Invitation File Document struct Document: FileDocument { + + public let invitation: NewKey.Invitation + + public init(invitation: NewKey.Invitation) { + self.invitation = invitation + } + + internal static let decoder = JSONDecoder() + + internal static let encoder = JSONEncoder() /// The types the document is able to open. public static var readableContentTypes: [UTType] { - return [.json] + return [.json, UTType(exportedAs: NewKey.Invitation.documentType)] } public init(configuration: ReadConfiguration) throws { - fatalError() + guard let data = configuration.file.regularFileContents, + let invitation = try? Self.decoder.decode(NewKey.Invitation.self, from: data) + else { throw CocoaError(.fileReadCorruptFile) } + self.init(invitation: invitation) } public func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { - fatalError() + let data = try Self.encoder.encode(invitation) + return .init(regularFileWithContents: data) } } } diff --git a/Xcode/LockKit/View/NavigationLink.swift b/Xcode/LockKit/View/NavigationLink.swift index 15d950c2..dbc356bd 100644 --- a/Xcode/LockKit/View/NavigationLink.swift +++ b/Xcode/LockKit/View/NavigationLink.swift @@ -92,7 +92,7 @@ public extension AppNavigationLinkID { } } -public struct AppNavigationDestinationView: View { +public struct AppNavigationDestinationView: View, Identifiable { public let id: AppNavigationLinkID diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 09a4e9b1..2997e120 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -90,12 +90,12 @@ 6E5D51DF28E20C6D008FFB4D /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E5D51C128E185DB008FFB4D /* QuickLook.framework */; }; 6E5D51E228E20C6D008FFB4D /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D51E128E20C6D008FFB4D /* PreviewViewController.swift */; }; 6E5D51E728E20C6D008FFB4D /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6E5D51E528E20C6D008FFB4D /* MainInterface.storyboard */; }; - 6E5D51EB28E20C6D008FFB4D /* LockQuickLook.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6E5D51DE28E20C6C008FFB4D /* LockQuickLook.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 6E5D51EB28E20C6D008FFB4D /* LockQuickLookMobile.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6E5D51DE28E20C6C008FFB4D /* LockQuickLookMobile.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 6E5D51F028E20E8E008FFB4D /* LockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; }; 6E5D51FB28E21538008FFB4D /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E5D51FA28E21537008FFB4D /* Quartz.framework */; }; 6E5D51FE28E21538008FFB4D /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D51FD28E21538008FFB4D /* PreviewViewController.swift */; }; 6E5D520328E21538008FFB4D /* PreviewViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6E5D520128E21538008FFB4D /* PreviewViewController.xib */; }; - 6E5D520828E21538008FFB4D /* LockQuickLook.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6E5D51F928E21537008FFB4D /* LockQuickLook.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 6E5D520828E21538008FFB4D /* LockQuickLook.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6E5D51F928E21537008FFB4D /* LockQuickLook.appex */; platformFilters = (macos, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 6E5D520C28E215E9008FFB4D /* LockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; }; 6E6A97EF28DBEC2D00C689F6 /* NewKeyInvitationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */; }; 6E6A97F328DC030000C689F6 /* NewKeyInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */; }; @@ -190,7 +190,7 @@ dstSubfolderSpec = 13; files = ( 6E5D520828E21538008FFB4D /* LockQuickLook.appex in Embed Foundation Extensions */, - 6E5D51EB28E20C6D008FFB4D /* LockQuickLook.appex in Embed Foundation Extensions */, + 6E5D51EB28E20C6D008FFB4D /* LockQuickLookMobile.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -288,7 +288,7 @@ 6E4CB62228D7907E00116573 /* PermissionIconViewNSView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionIconViewNSView.swift; sourceTree = ""; }; 6E4CB62428D792BF00116573 /* InfoPlist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InfoPlist.swift; path = ../../../iOS/SmartLock/InfoPlist.swift; sourceTree = ""; }; 6E5D51C128E185DB008FFB4D /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = System/Library/Frameworks/QuickLook.framework; sourceTree = SDKROOT; }; - 6E5D51DE28E20C6C008FFB4D /* LockQuickLook.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LockQuickLook.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 6E5D51DE28E20C6C008FFB4D /* LockQuickLookMobile.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LockQuickLookMobile.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6E5D51E128E20C6D008FFB4D /* PreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewViewController.swift; sourceTree = ""; }; 6E5D51E628E20C6D008FFB4D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; 6E5D51E828E20C6D008FFB4D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -678,7 +678,7 @@ 6EA7769D28D707FE00018FA3 /* LockKit.framework */, 6E3276C128D70A3700AF171B /* MatterLock.appex */, 6E8BBFE828DD301B00F03735 /* LockIntents.appex */, - 6E5D51DE28E20C6C008FFB4D /* LockQuickLook.appex */, + 6E5D51DE28E20C6C008FFB4D /* LockQuickLookMobile.appex */, 6E5D51F928E21537008FFB4D /* LockQuickLook.appex */, ); name = Products; @@ -775,7 +775,7 @@ ); name = "LockQuickLook-iOS"; productName = LockQuickLook; - productReference = 6E5D51DE28E20C6C008FFB4D /* LockQuickLook.appex */; + productReference = 6E5D51DE28E20C6C008FFB4D /* LockQuickLookMobile.appex */; productType = "com.apple.product-type.app-extension"; }; 6E5D51F828E21537008FFB4D /* LockQuickLook-macOS */ = { @@ -1138,6 +1138,9 @@ }; 6E5D520728E21538008FFB4D /* PBXTargetDependency */ = { isa = PBXTargetDependency; + platformFilters = ( + macos, + ); target = 6E5D51F828E21537008FFB4D /* LockQuickLook-macOS */; targetProxy = 6E5D520628E21538008FFB4D /* PBXContainerItemProxy */; }; @@ -1250,7 +1253,7 @@ ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.QuickLook; - PRODUCT_NAME = LockQuickLook; + PRODUCT_NAME = LockQuickLookMobile; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1282,7 +1285,7 @@ ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.QuickLook; - PRODUCT_NAME = LockQuickLook; + PRODUCT_NAME = LockQuickLookMobile; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1311,6 +1314,11 @@ "$(inherited)", "@executable_path/../Frameworks", "@executable_path/../../../../Frameworks", + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + "@executable_path/../../../Frameworks", + "@executable_path/../../../../Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.QuickLook; @@ -1341,6 +1349,11 @@ "$(inherited)", "@executable_path/../Frameworks", "@executable_path/../../../../Frameworks", + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + "@executable_path/../../../Frameworks", + "@executable_path/../../../../Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.QuickLook; diff --git a/Xcode/SmartLock/App.swift b/Xcode/SmartLock/App.swift index 36f99be7..a6ee1526 100644 --- a/Xcode/SmartLock/App.swift +++ b/Xcode/SmartLock/App.swift @@ -26,6 +26,7 @@ struct LockApp: App { #endif var body: some Scene { + // main window WindowGroup { ContentView() @@ -33,9 +34,15 @@ struct LockApp: App { .environment(\.managedObjectContext, Store.shared.managedObjectContext) } + // documents + DocumentGroup(viewing: NewKey.Invitation.Document.self) { file in + NewKeyInvitationView(invitation: file.document.invitation) + .environmentObject(Store.shared) + } + #if os(macOS) Window("Nearby", id: "nearby") { - NavigationStack { + NavigationView { NearbyDevicesView() .navigationDestination(for: AppNavigationLinkID.self) { AppNavigationDestinationView(id: $0) @@ -46,7 +53,7 @@ struct LockApp: App { } Window("Keys", id: "keys") { - NavigationStack { + NavigationView { KeysView() .navigationDestination(for: AppNavigationLinkID.self) { AppNavigationDestinationView(id: $0) @@ -75,6 +82,10 @@ struct LockApp: App { #if os(iOS) final class AppDelegate: UIResponder, UIApplicationDelegate { + let appLaunch = Date() + + private(set) var didBecomeActive: Bool = false + // MARK: - UIApplicationDelegate func application( @@ -85,26 +96,223 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { // set app appearance //UIView.configureLockAppearance() + #if DEBUG + defer { log("App finished launching in \(String(format: "%.3f", Date().timeIntervalSince(appLaunch)))s") } + #endif + + // load store singleton + let _ = Store.shared + + // queue post-app initialization loading + Task { + //let _ = NetworkMonitor.shared + // subscribe to push notifications + //self.queueDidLaunchOperations() + } + return true } - func applicationDidBecomeActive( - _ application: UIApplication - ) { + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + + log("Will resign active") + } + + func applicationDidEnterBackground(_ application: UIApplication) { + + // only scan and sync in background when low power mode is disabled. + guard ProcessInfo.processInfo.isLowPowerModeEnabled == false else { return } + + // scan in background + Task { + if await Store.shared.central.state == .poweredOn { + let bluetoothTask = application.beginBackgroundTask(withName: "BluetoothScan", expirationHandler: { + log("Bluetooth Scan background task expired") + }) + // scan for nearby devices + do { try await Store.shared.scan(duration: 1.0) } + catch { log("⚠️ Unable to scan: \(error.localizedDescription)") } + // read information characteristic + for device in Store.shared.peripherals.keys { + guard Store.shared.lockInformation[device] == nil + else { continue } + do { try await Store.shared.readInformation(for: device) } + catch { log("⚠️ Unable to read information: \(error.localizedDescription)") } + } + + await MainActor.run { + self.logBackgroundTimeRemaining() + log("Bluetooth background task ended") + application.endBackgroundTask(bluetoothTask) + } + } + } + // attempt to sync with iCloud in background + let cloudTask = application.beginBackgroundTask(withName: "iCloudSync", expirationHandler: { + log("iCloud Sync background task expired") + }) Task { do { try await Store.shared.syncCloud() } - catch { log("⚠️ Unable to automatically sync with iCloud. \(error)") } + catch { log("⚠️ Unable to sync: \(error.localizedDescription)") } + await MainActor.run { + self.logBackgroundTimeRemaining() + log("iCloud background task ended") + application.endBackgroundTask(cloudTask) + } } } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + + log("Will enter foreground") + + // save energy + guard ProcessInfo.processInfo.isLowPowerModeEnabled == false else { return } + + Task { + // attempt to scan for all known locks if they are not in central cache + if await Store.shared.central.state == .poweredOn { + let locks = Store.shared.applicationData.locks.keys + for lock in locks { + guard let peripheral = try? await Store.shared.device(for: lock) else { + continue + } + guard Store.shared.lockInformation[peripheral] == nil + else { continue } + do { try await Store.shared.readInformation(for: peripheral) } + catch { log("⚠️ Unable to read information: \(error.localizedDescription)") } + } + } + } + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + + log("Did become active") + + didBecomeActive = true + application.applicationIconBadgeNumber = 0 + + // scan for iBeacons + //BeaconController.shared.scanBeacons() + + // save energy + guard ProcessInfo.processInfo.isLowPowerModeEnabled == false else { return } + + // attempt to sync with iCloud + //tabBarController.syncCloud() + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + + log("Will terminate") + + // scan for iBeacons + //BeaconController.shared.scanBeacons() + } + + func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + + Task { + let result = await applicationPerformFetch(application) + completionHandler(result) + } + } + + private func applicationPerformFetch(_ application: UIApplication) async -> UIBackgroundFetchResult { + + log("Perform background fetch") + logBackgroundTimeRemaining() + defer { log("Background fetch ended") } + + //BeaconController.shared.scanBeacons() + + //let lockInformation = Array(Store.shared.lockInformation.values) + + // 30 sec max background fetch + let scanTask = Task { () -> UIBackgroundFetchResult in + do { + // scan for locks + try await Store.shared.scan(duration: 3.0) + // make sure each stored lock is visible + let locks = Store.shared.applicationData.locks + .lazy + .sorted { $0.value.key.created < $1.value.key.created } + .map { $0.key } + .prefix(10) + // scan for locks not found + for lock in locks { + let _ = try await Store.shared.device(for: lock, scanDuration: 1.0) + } + return .newData + } catch { + log("⚠️ Unable to scan: \(error.localizedDescription)") + return .failed + } + } + + let cloudTask = Task { () -> UIBackgroundFetchResult in + do { + guard try await Store.shared.cloud.accountStatus() == .available else { + return .noData + } + try await Store.shared.syncCloud() + return .newData + } + catch { + log("⚠️ Unable to sync: \(error.localizedDescription)") + return .failed + } + } + + await MainActor.run { + self.logBackgroundTimeRemaining() + } + + async let results = [ + scanTask.value, + cloudTask.value + ] + + if await results.contains(.failed) { + return .failed + } else if await results.contains(.newData) { + return .newData + } else { + return .noData + } + } +} + +private extension AppDelegate { + + private static let intervalFormatter: DateIntervalFormatter = { + let formatter = DateIntervalFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .medium + return formatter + }() + + func logBackgroundTimeRemaining() { + + let backgroundTimeRemaining = UIApplication.shared.backgroundTimeRemaining + let start = Date() + let timeString = Self.intervalFormatter.string(from: start, to: start + backgroundTimeRemaining) + log("Background time remaining: \(timeString)") + } } + #elseif os(macOS) final class AppDelegate: NSResponder, NSApplicationDelegate { // MARK: - NSApplicationDelegate - - - + func applicationShouldTerminateAfterLastWindowClosed( _ sender: NSApplication ) -> Bool { diff --git a/Xcode/SmartLock/View/TabBarView.swift b/Xcode/SmartLock/View/TabBarView.swift index e1733bbe..ffdd10d9 100644 --- a/Xcode/SmartLock/View/TabBarView.swift +++ b/Xcode/SmartLock/View/TabBarView.swift @@ -15,6 +15,12 @@ struct TabBarView: View { //@State //private var columnVisibility: NavigationSplitViewVisibility = .doubleColumn + @State + private var sheet: AppNavigationDestinationView? + + @State + private var error: Error? + var body: some View { TabView { @@ -54,6 +60,66 @@ struct TabBarView: View { .navigationViewStyle(.stack) } .navigationBarTitleDisplayMode(.large) + .onOpenURL { url in + open(url: url) + } + .sheet(item: $sheet) { view in + NavigationView { + view + .toolbar { + ToolbarItem(placement: .navigation) { + Text("Cancel") + } + } + } + } + .alert(error: $error) + } +} + +extension TabBarView { + + func open(url: URL) { + self.error = nil + log("Open \(url.description)") + Task { + do { + try await open(url: url) + } + catch { + log("⚠️ Unable to open URL. \(error.localizedDescription)") + // show error + self.error = error + } + } + } + + func open(url: URL) async throws { + + if url.isFileURL { + try await open(file: url) + } else if let lockURL = LockURL(rawValue: url) { + try open(url: lockURL) + } else { + throw CocoaError(.fileReadInvalidFileName) + } + } + + func open(file url: URL) async throws { + let data = try Data(contentsOf: url, options: [.mappedIfSafe]) + let decoder = JSONDecoder() + let invitation = try decoder.decode(NewKey.Invitation.self, from: data) + await open(invitation: invitation) + } + + @MainActor + func open(invitation: NewKey.Invitation) { + self.sheet = .init(id: .newKeyInvitation(invitation)) + } + + func open(url: LockURL) throws { + // deep link + assertionFailure() } } From c88fb553af9b9fb1a89a5de10252af4e5578382b Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 26 Sep 2022 22:13:37 -0700 Subject: [PATCH 180/229] [App] Updated `SetupLockView` --- Xcode/LockKit/View/LockDetailView.swift | 6 +- Xcode/LockKit/View/SetupLockView.swift | 214 +++++++++++++++++- Xcode/SmartLock.xcodeproj/project.pbxproj | 17 ++ .../xcshareddata/swiftpm/Package.resolved | 9 + .../xcschemes/LockQuickLook-iOS.xcscheme | 2 +- Xcode/SmartLock/View/TabBarView.swift | 32 ++- 6 files changed, 267 insertions(+), 13 deletions(-) diff --git a/Xcode/LockKit/View/LockDetailView.swift b/Xcode/LockKit/View/LockDetailView.swift index 18e20151..311b417c 100644 --- a/Xcode/LockKit/View/LockDetailView.swift +++ b/Xcode/LockKit/View/LockDetailView.swift @@ -100,11 +100,7 @@ public struct LockDetailView: View { ) } else if let information = self.information, information.status == .setup { - #if os(iOS) - AnyView(SetupLockView(id: id)) - #else - AnyView(Text("Setup this lock on your iOS device.")) - #endif + AnyView(SetupLockView()) } else { AnyView(UnknownView(id: id, information: information)) } diff --git a/Xcode/LockKit/View/SetupLockView.swift b/Xcode/LockKit/View/SetupLockView.swift index 3782c916..21741b39 100644 --- a/Xcode/LockKit/View/SetupLockView.swift +++ b/Xcode/LockKit/View/SetupLockView.swift @@ -5,20 +5,226 @@ // Created by Alsey Coleman Miller on 9/21/22. // +import Foundation import SwiftUI import CoreLock +import SFSafeSymbols +#if os(iOS) +import CodeScanner +#endif /// View for lock setup. public struct SetupLockView: View { - public let id: UUID + private let success: ((UUID) -> ())? + + @State + private var state: SetupState = .camera + + public init( + success: ((UUID) -> ())? = nil + ) { + self.success = success + self.state = .camera + } - public init(id: UUID) { - self.id = id + public init( + lock: UUID, + sharedSecret: KeyData, + success: ((UUID) -> ())? = nil + ) { + self.success = success + self.state = .confirm(lock, sharedSecret) } public var body: some View { - Text("Setup this lock on your iOS device.") + switch state { + case .camera: + AnyView( + CameraView(completion: scanResult) + ) + case let .confirm(lock, key): + AnyView( + ConfirmView(lock: lock) { name in + setup(lock: lock, using: key, name: name) + } + ) + case let .loading(lock, key, name): + AnyView( + LoadingView( + lock: lock, + name: name + ) + ) + case let .error(error): + AnyView( + ErrorView( + error: error, + retry: retry + ) + ) + case let .success(lock, name): + AnyView( + SuccessView( + lock: lock, + name: name, + completion: success + ) + ) + } + } +} + +private extension SetupLockView { + + func scanResult(_ result: Result) { + switch result { + case let .success(scanResult): + guard let url = URL(string: scanResult.string), + let lockURL = LockURL(rawValue: url), + case let .setup(lock, secret) = lockURL + else { self.state = .error(LockError.invalidQRCode); return } + self.state = .confirm(lock, secret) + case let .failure(error): + self.state = .error(error) + } + } + + func setup(lock: UUID, using sharedSecret: KeyData, name: String) { + self.state = .loading(lock, name) + Task { + do { + guard let peripheral = try await Store.shared.device(for: lock) else { + throw LockError.notInRange(lock: lock) + } + try await Store.shared.setup( + for: peripheral, + using: sharedSecret, + name: name + ) + self.state = .success(lock, name) + } catch { + self.state = .error(error) + } + } + } + + func retry() { + self.state = .camera + } + + +} + +internal extension SetupLockView { + + enum SetupState { + + case camera + case confirm(UUID, KeyData) + case loading(UUID, String) + case success(UUID, String) + case error(Error) + } +} + +internal extension SetupLockView { + + struct CameraView: View { + + let completion: ((Result) -> ()) + + var body: some View { + #if os(iOS) && !targetEnvironment(simulator) + AnyView( + CodeScannerView(codeTypes: [.qr], completion: completion) + ) + #else + AnyView(Text("Setup this lock on your iOS device.")) + #endif + } + } + + struct ConfirmView: View { + + let lock: UUID + + let confirm: (String) -> () + + @State + private var name: String = "" + + var body: some View { + VStack(alignment: .center, spacing: 16) { + TextField("Lock Name", text: $name, prompt: Text("My Lock")) + Button("Configure") { + confirm(name) + } + } + } + } + + struct LoadingView: View { + + let lock: UUID + + let name: String + + var body: some View { + VStack(alignment: .center, spacing: 16) { + Text("Configuring lock...") + ProgressView() + .progressViewStyle(.circular) + } + } + } + + struct SuccessView: View { + + let lock: UUID + + let name: String + + let completion: ((UUID) -> ())? + + var body: some View { + VStack(alignment: .center, spacing: 16) { + Image(systemSymbol: .checkmarkCircleFill) + .symbolRenderingMode(.palette) + .accentColor(.green) + Text("Successfully setup \(name).") + ProgressView() + .progressViewStyle(.circular) + } + .toolbar { + ToolbarItem(placement: .primaryAction) { + if let completion = self.completion { + Button("Done") { + completion(lock) + } + } + } + } + } + } + + struct ErrorView: View { + + let error: Error + + let retry: () -> () + + var body: some View { + VStack(alignment: .center, spacing: 16) { + Image(systemSymbol: .exclamationmarkOctagonFill) + .symbolRenderingMode(.multicolor) + Text("Error") + Text(verbatim: error.localizedDescription) + Button(action: retry) { + Text("Retry") + } + } + } } } diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 2997e120..226b2bab 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -97,6 +97,7 @@ 6E5D520328E21538008FFB4D /* PreviewViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6E5D520128E21538008FFB4D /* PreviewViewController.xib */; }; 6E5D520828E21538008FFB4D /* LockQuickLook.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6E5D51F928E21537008FFB4D /* LockQuickLook.appex */; platformFilters = (macos, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 6E5D520C28E215E9008FFB4D /* LockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; }; + 6E5D521328E29825008FFB4D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 6E5D521228E29825008FFB4D /* CodeScanner */; }; 6E6A97EF28DBEC2D00C689F6 /* NewKeyInvitationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */; }; 6E6A97F328DC030000C689F6 /* NewKeyInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */; }; 6E84E52728DDC841008CAE85 /* ScanLocksIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFEC28DD301B00F03735 /* ScanLocksIntent.swift */; }; @@ -388,6 +389,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6E5D521328E29825008FFB4D /* CodeScanner in Frameworks */, 6E9E37B228DB852A00BE7128 /* Predicate in Frameworks */, 6E169A8428D9C25C008545EC /* SFSafeSymbols in Frameworks */, 6E21830A28D7FEA900A622B3 /* KeychainAccess in Frameworks */, @@ -858,6 +860,7 @@ 6E21836928D953D000A622B3 /* CloudKitCodable */, 6E169A8328D9C25C008545EC /* SFSafeSymbols */, 6E9E37B128DB852A00BE7128 /* Predicate */, + 6E5D521228E29825008FFB4D /* CodeScanner */, ); productName = LockKit; productReference = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; @@ -909,6 +912,7 @@ 6E21836828D953D000A622B3 /* XCRemoteSwiftPackageReference "CloudKitCodable" */, 6E169A8228D9C25C008545EC /* XCRemoteSwiftPackageReference "SFSafeSymbols" */, 6E9E37B028DB852A00BE7128 /* XCRemoteSwiftPackageReference "Predicate" */, + 6E5D521128E29825008FFB4D /* XCRemoteSwiftPackageReference "CodeScanner" */, ); productRefGroup = 6EA7768228D7061600018FA3 /* Products */; projectDirPath = ""; @@ -1831,6 +1835,14 @@ kind = branch; }; }; + 6E5D521128E29825008FFB4D /* XCRemoteSwiftPackageReference "CodeScanner" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/twostraws/CodeScanner.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.0; + }; + }; 6E9E37B028DB852A00BE7128 /* XCRemoteSwiftPackageReference "Predicate" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/PureSwift/Predicate"; @@ -1871,6 +1883,11 @@ package = 6E3276D528D70FA000AF171B /* XCRemoteSwiftPackageReference "GATT" */; productName = GATT; }; + 6E5D521228E29825008FFB4D /* CodeScanner */ = { + isa = XCSwiftPackageProductDependency; + package = 6E5D521128E29825008FFB4D /* XCRemoteSwiftPackageReference "CodeScanner" */; + productName = CodeScanner; + }; 6E9E37B128DB852A00BE7128 /* Predicate */ = { isa = XCSwiftPackageProductDependency; package = 6E9E37B028DB852A00BE7128 /* XCRemoteSwiftPackageReference "Predicate" */; diff --git a/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 60874f94..9b07d12e 100644 --- a/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,6 +27,15 @@ "revision" : "598e04c5c6f1162d51fdc45d11c85574d39ee85c" } }, + { + "identity" : "codescanner", + "kind" : "remoteSourceControl", + "location" : "https://github.com/twostraws/CodeScanner.git", + "state" : { + "revision" : "c7859712034a08bb5a018fbe8751c1f1edc4b248", + "version" : "2.2.1" + } + }, { "identity" : "gatt", "kind" : "remoteSourceControl", diff --git a/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockQuickLook-iOS.xcscheme b/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockQuickLook-iOS.xcscheme index 4a8406c1..9d605a2c 100644 --- a/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockQuickLook-iOS.xcscheme +++ b/Xcode/SmartLock.xcodeproj/xcshareddata/xcschemes/LockQuickLook-iOS.xcscheme @@ -16,7 +16,7 @@ diff --git a/Xcode/SmartLock/View/TabBarView.swift b/Xcode/SmartLock/View/TabBarView.swift index ffdd10d9..6faa50e5 100644 --- a/Xcode/SmartLock/View/TabBarView.swift +++ b/Xcode/SmartLock/View/TabBarView.swift @@ -12,6 +12,9 @@ import SFSafeSymbols struct TabBarView: View { + @EnvironmentObject + var store: Store + //@State //private var columnVisibility: NavigationSplitViewVisibility = .doubleColumn @@ -99,7 +102,7 @@ extension TabBarView { if url.isFileURL { try await open(file: url) } else if let lockURL = LockURL(rawValue: url) { - try open(url: lockURL) + try await open(url: lockURL) } else { throw CocoaError(.fileReadInvalidFileName) } @@ -117,9 +120,32 @@ extension TabBarView { self.sheet = .init(id: .newKeyInvitation(invitation)) } + @MainActor + func open(lock: UUID) { + self.sheet = .init(id: .lock(lock)) + } + + @MainActor func open(url: LockURL) throws { - // deep link - assertionFailure() + switch url { + case let .newKey(invitation): + open(invitation: invitation) + case let .unlock(lock: lock): + // Deep link + open(lock: lock) + case let .setup(lock: lock, secret: secretData): + // setup + //self.sheet = .init(id: ) + Task { + guard store.state == .poweredOn else { + throw LockError.bluetoothUnavailable + } + guard let peripheral = try await store.device(for: lock) else { + throw LockError.notInRange(lock: lock) + } + try await store.setup(for: peripheral, using: secretData, name: "My Lock") + } + } } } From 1f17062626416d4e6205605fd8606c5744d5db3a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 26 Sep 2022 23:35:02 -0700 Subject: [PATCH 181/229] [App] Fixed setup --- Xcode/LockKit/View/NavigationLink.swift | 8 +++ Xcode/LockKit/View/SetupLockView.swift | 85 ++++++++++++++----------- Xcode/SmartLock/View/TabBarView.swift | 28 ++++---- 3 files changed, 66 insertions(+), 55 deletions(-) diff --git a/Xcode/LockKit/View/NavigationLink.swift b/Xcode/LockKit/View/NavigationLink.swift index dbc356bd..f5c9687f 100644 --- a/Xcode/LockKit/View/NavigationLink.swift +++ b/Xcode/LockKit/View/NavigationLink.swift @@ -55,6 +55,7 @@ private extension AppNavigationLink { public enum AppNavigationLinkID: Hashable { case lock(UUID) + case setup(UUID, KeyData) case events(UUID, LockEvent.Predicate?) case permissions(UUID) case key(UUID, KeyDetailView.Value) @@ -65,6 +66,7 @@ public enum AppNavigationLinkID: Hashable { public enum AppNavigationLinkType: String { case lock + case setup case events case permissions case key @@ -78,6 +80,8 @@ public extension AppNavigationLinkID { switch self { case .lock: return .lock + case .setup: + return .setup case .events: return .events case .permissions: @@ -106,6 +110,10 @@ public struct AppNavigationDestinationView: View, Identifiable { AnyView( LockDetailView(id: id) ) + case let .setup(id, sharedSecret): + AnyView( + SetupLockView(lock: id, sharedSecret: sharedSecret) + ) case let .events(lock, predicate): AnyView( EventsView(lock: lock, predicate: predicate) diff --git a/Xcode/LockKit/View/SetupLockView.swift b/Xcode/LockKit/View/SetupLockView.swift index 21741b39..830e1b3c 100644 --- a/Xcode/LockKit/View/SetupLockView.swift +++ b/Xcode/LockKit/View/SetupLockView.swift @@ -16,6 +16,9 @@ import CodeScanner /// View for lock setup. public struct SetupLockView: View { + @EnvironmentObject + public var store: Store + private let success: ((UUID) -> ())? @State @@ -38,40 +41,8 @@ public struct SetupLockView: View { } public var body: some View { - switch state { - case .camera: - AnyView( - CameraView(completion: scanResult) - ) - case let .confirm(lock, key): - AnyView( - ConfirmView(lock: lock) { name in - setup(lock: lock, using: key, name: name) - } - ) - case let .loading(lock, key, name): - AnyView( - LoadingView( - lock: lock, - name: name - ) - ) - case let .error(error): - AnyView( - ErrorView( - error: error, - retry: retry - ) - ) - case let .success(lock, name): - AnyView( - SuccessView( - lock: lock, - name: name, - completion: success - ) - ) - } + stateView + .navigationTitle("Setup") } } @@ -94,10 +65,13 @@ private extension SetupLockView { self.state = .loading(lock, name) Task { do { - guard let peripheral = try await Store.shared.device(for: lock) else { + guard store.state == .poweredOn else { + throw LockError.bluetoothUnavailable + } + guard let peripheral = try await store.device(for: lock) else { throw LockError.notInRange(lock: lock) } - try await Store.shared.setup( + try await store.setup( for: peripheral, using: sharedSecret, name: name @@ -113,7 +87,42 @@ private extension SetupLockView { self.state = .camera } - + var stateView: some View { + switch state { + case .camera: + return AnyView( + CameraView(completion: scanResult) + ) + case let .confirm(lock, sharedSecret): + return AnyView( + ConfirmView(lock: lock) { name in + setup(lock: lock, using: sharedSecret, name: name) + } + ) + case let .loading(lock, name): + return AnyView( + LoadingView( + lock: lock, + name: name + ) + ) + case let .error(error): + return AnyView( + ErrorView( + error: error, + retry: retry + ) + ) + case let .success(lock, name): + return AnyView( + SuccessView( + lock: lock, + name: name, + completion: success + ) + ) + } + } } internal extension SetupLockView { @@ -231,7 +240,7 @@ internal extension SetupLockView { #if DEBUG struct SetupLockView_Previews: PreviewProvider { static var previews: some View { - EmptyView() + SetupLockView() } } #endif diff --git a/Xcode/SmartLock/View/TabBarView.swift b/Xcode/SmartLock/View/TabBarView.swift index 6faa50e5..7492d8c3 100644 --- a/Xcode/SmartLock/View/TabBarView.swift +++ b/Xcode/SmartLock/View/TabBarView.swift @@ -80,6 +80,7 @@ struct TabBarView: View { } } +@MainActor extension TabBarView { func open(url: URL) { @@ -102,7 +103,7 @@ extension TabBarView { if url.isFileURL { try await open(file: url) } else if let lockURL = LockURL(rawValue: url) { - try await open(url: lockURL) + try open(url: lockURL) } else { throw CocoaError(.fileReadInvalidFileName) } @@ -112,40 +113,33 @@ extension TabBarView { let data = try Data(contentsOf: url, options: [.mappedIfSafe]) let decoder = JSONDecoder() let invitation = try decoder.decode(NewKey.Invitation.self, from: data) - await open(invitation: invitation) + open(invitation: invitation) } - @MainActor func open(invitation: NewKey.Invitation) { self.sheet = .init(id: .newKeyInvitation(invitation)) } - @MainActor func open(lock: UUID) { self.sheet = .init(id: .lock(lock)) } - @MainActor func open(url: LockURL) throws { switch url { case let .newKey(invitation): open(invitation: invitation) case let .unlock(lock: lock): - // Deep link open(lock: lock) case let .setup(lock: lock, secret: secretData): - // setup - //self.sheet = .init(id: ) - Task { - guard store.state == .poweredOn else { - throw LockError.bluetoothUnavailable - } - guard let peripheral = try await store.device(for: lock) else { - throw LockError.notInRange(lock: lock) - } - try await store.setup(for: peripheral, using: secretData, name: "My Lock") - } + try setup(lock: lock, using: secretData) + } + } + + func setup(lock: UUID, using secretData: KeyData) throws { + guard store.applicationData.locks[lock] == nil else { + throw LockError.existingKey(lock: lock) } + self.sheet = .init(id: .setup(lock, secretData)) } } From 41220c2fd051b569dd26bfbd53a5e5cdade8f798 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 27 Sep 2022 08:52:41 -0700 Subject: [PATCH 182/229] [App] Updated `PermissionsView` --- Xcode/LockKit/View/PermissionsView.swift | 29 +++++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/Xcode/LockKit/View/PermissionsView.swift b/Xcode/LockKit/View/PermissionsView.swift index 905181e6..9b9b853e 100644 --- a/Xcode/LockKit/View/PermissionsView.swift +++ b/Xcode/LockKit/View/PermissionsView.swift @@ -78,7 +78,9 @@ public struct PermissionsView: View { keys: keys.lazy.compactMap { Key(managedObject: $0) }, newKeys: newKeys.lazy.compactMap { NewKey(managedObject: $0) }, invitations: invitations, - reload: reload + reload: reload, + deleteKeys: deleteKeys, + deleteNewKeys: deleteNewKeys ) .onAppear { self.keys.nsPredicate = predicate @@ -136,7 +138,6 @@ private extension PermissionsView { for (url, invitation) in cache { invitations[invitation.key.id] = url } - assert(invitations.count == cache.count) return invitations } @@ -203,11 +204,19 @@ private extension PermissionsView { reload() } } + + func deleteKeys(_ keys: [Key]) { + + } + + func deleteNewKeys(_ keys: [NewKey]) { + + } } internal extension PermissionsView { - struct StateView : View where Keys: RandomAccessCollection, Keys.Element == Key, NewKeys: RandomAccessCollection, NewKeys.Element == NewKey { + struct StateView : View where Keys: RandomAccessCollection, Keys.Element == Key, Keys.Index == Int, NewKeys: RandomAccessCollection, NewKeys.Element == NewKey, NewKeys.Index == Int { let lock: UUID @@ -219,6 +228,10 @@ internal extension PermissionsView { let reload: () -> () + let deleteKeys: ([Key]) -> () + + let deleteNewKeys: ([NewKey]) -> () + var body: some View { List { ForEach(keys) { @@ -311,11 +324,13 @@ private extension PermissionsView.StateView { } func deleteKey(at indexSet: IndexSet) { - + let keys = indexSet.map { self.keys[$0] } + deleteKeys(keys) } func deleteNewKey(at indexSet: IndexSet) { - + let files = indexSet.map { self.newKeys[$0] } + deleteNewKeys(files) } } @@ -373,7 +388,9 @@ struct PermissionsView_Previews: PreviewProvider { UUID(uuidString: "ED6DE87A-D0AF-421B-912D-3400A60EB294")! : URL(fileURLWithPath: "/tmp/newKey-\(UUID(uuidString: "ED6DE87A-D0AF-421B-912D-3400A60EB294")!).ekey") ], - reload: { } + reload: { }, + deleteKeys: { _ in }, + deleteNewKeys: { _ in } ) } } From 6cfedbaff17f86a52087044a0d9067299e841fc9 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 27 Sep 2022 08:52:54 -0700 Subject: [PATCH 183/229] [App] Updated extension version --- Xcode/SmartLock.xcodeproj/project.pbxproj | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 226b2bab..c43ce6eb 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -1242,7 +1242,6 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4W79SG34MW; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "LockQuickLook-iOS/Info.plist"; @@ -1255,7 +1254,6 @@ "@executable_path/../../../Frameworks", "@executable_path/../../../../Frameworks", ); - MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.QuickLook; PRODUCT_NAME = LockQuickLookMobile; SDKROOT = iphoneos; @@ -1274,7 +1272,6 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4W79SG34MW; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "LockQuickLook-iOS/Info.plist"; @@ -1287,7 +1284,6 @@ "@executable_path/../../../Frameworks", "@executable_path/../../../../Frameworks", ); - MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.QuickLook; PRODUCT_NAME = LockQuickLookMobile; SDKROOT = iphoneos; @@ -1307,7 +1303,6 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4W79SG34MW; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -1324,7 +1319,6 @@ "@executable_path/../../../Frameworks", "@executable_path/../../../../Frameworks", ); - MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.QuickLook; PRODUCT_NAME = LockQuickLook; SDKROOT = macosx; @@ -1342,7 +1336,6 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4W79SG34MW; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -1359,7 +1352,6 @@ "@executable_path/../../../Frameworks", "@executable_path/../../../../Frameworks", ); - MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.QuickLook; PRODUCT_NAME = LockQuickLook; SDKROOT = macosx; From be2a5e93fbaadecf1e03e94f9d391e769726508a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 27 Sep 2022 10:09:05 -0700 Subject: [PATCH 184/229] [App] Added Thumbnail extension --- Xcode/LockKit/Model/AssetExtractor.swift | 23 +- Xcode/LockKit/View/SetupLockView.swift | 16 +- Xcode/LockKit/View/UIKit/ActivityItem.swift | 1 + Xcode/LockThumbnail/Info.plist | 22 ++ Xcode/LockThumbnail/ThumbnailProvider.swift | 99 ++++++ Xcode/SmartLock.xcodeproj/project.pbxproj | 314 +++++++++++++++++++- 6 files changed, 461 insertions(+), 14 deletions(-) create mode 100644 Xcode/LockThumbnail/Info.plist create mode 100644 Xcode/LockThumbnail/ThumbnailProvider.swift diff --git a/Xcode/LockKit/Model/AssetExtractor.swift b/Xcode/LockKit/Model/AssetExtractor.swift index c1962510..1bfe1c71 100644 --- a/Xcode/LockKit/Model/AssetExtractor.swift +++ b/Xcode/LockKit/Model/AssetExtractor.swift @@ -5,11 +5,16 @@ // Created by Alsey Coleman Miller on 9/23/22. // -#if canImport(UIKit) import Foundation +#if canImport(UIKit) import UIKit +#elseif canImport(AppKit) +import AppKit +#endif +import SwiftUI /// Get URL from asset. +@MainActor @available(iOS 8.0, watchOS 6.0, *) public final class AssetExtractor { @@ -25,21 +30,27 @@ public final class AssetExtractor { return url }() - @available(iOS 8.0, watchOS 6.0, *) public func url(for imageName: String, in bundle: Bundle = .lockKit) -> URL? { let fileName = (bundle.bundleIdentifier ?? bundle.bundleURL.lastPathComponent) + "." + imageName + ".png" let url = cachesDirectory.appendingPathComponent(fileName) if fileManager.fileExists(atPath: url.path) == false { - guard let image = UIImage(named: imageName, in: bundle, compatibleWith: nil) + #if canImport(UIKit) + guard let image = UIImage(named: imageName, in: bundle, compatibleWith: nil), + let imageData = image.pngData() else { return nil } - guard let imageData = image.pngData() - else { fatalError("Could not convert image to PNG") } + #elseif canImport(AppKit) + let image = Image(imageName, bundle: bundle) + let imageRenderer = ImageRenderer(content: image) + guard let imageData = imageRenderer.cgImage + .map({ NSBitmapImageRep(cgImage: $0) })? + .representation(using: .png, properties: [:]) + else { return nil } + #endif fileManager.createFile(atPath: url.path, contents: imageData, attributes: nil) } return url } } -#endif diff --git a/Xcode/LockKit/View/SetupLockView.swift b/Xcode/LockKit/View/SetupLockView.swift index 830e1b3c..059a4a9b 100644 --- a/Xcode/LockKit/View/SetupLockView.swift +++ b/Xcode/LockKit/View/SetupLockView.swift @@ -48,6 +48,7 @@ public struct SetupLockView: View { private extension SetupLockView { + #if os(iOS) func scanResult(_ result: Result) { switch result { case let .success(scanResult): @@ -60,6 +61,7 @@ private extension SetupLockView { self.state = .error(error) } } + #endif func setup(lock: UUID, using sharedSecret: KeyData, name: String) { self.state = .loading(lock, name) @@ -90,9 +92,11 @@ private extension SetupLockView { var stateView: some View { switch state { case .camera: - return AnyView( - CameraView(completion: scanResult) - ) + #if os(iOS) && !targetEnvironment(simulator) + return AnyView(CameraView(completion: scanResult)) + #else + return AnyView(Text("Setup this lock on your iOS device.")) + #endif case let .confirm(lock, sharedSecret): return AnyView( ConfirmView(lock: lock) { name in @@ -139,20 +143,18 @@ internal extension SetupLockView { internal extension SetupLockView { + #if os(iOS) struct CameraView: View { let completion: ((Result) -> ()) var body: some View { - #if os(iOS) && !targetEnvironment(simulator) AnyView( CodeScannerView(codeTypes: [.qr], completion: completion) ) - #else - AnyView(Text("Setup this lock on your iOS device.")) - #endif } } + #endif struct ConfirmView: View { diff --git a/Xcode/LockKit/View/UIKit/ActivityItem.swift b/Xcode/LockKit/View/UIKit/ActivityItem.swift index 9b438c4f..b08eaf4e 100644 --- a/Xcode/LockKit/View/UIKit/ActivityItem.swift +++ b/Xcode/LockKit/View/UIKit/ActivityItem.swift @@ -66,6 +66,7 @@ public final class NewKeyFileActivityItem: UIActivityItemProvider { return UIImage(permissionType: invitation.key.permission.type) } + @MainActor public override func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? { let imageName = PermissionType.Image(permissionType: invitation.key.permission.type) diff --git a/Xcode/LockThumbnail/Info.plist b/Xcode/LockThumbnail/Info.plist new file mode 100644 index 00000000..d70c1231 --- /dev/null +++ b/Xcode/LockThumbnail/Info.plist @@ -0,0 +1,22 @@ + + + + + NSExtension + + NSExtensionAttributes + + QLSupportedContentTypes + + com.colemancda.lock.key + + QLThumbnailMinimumDimension + 32 + + NSExtensionPointIdentifier + com.apple.quicklook.thumbnail + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).ThumbnailProvider + + + diff --git a/Xcode/LockThumbnail/ThumbnailProvider.swift b/Xcode/LockThumbnail/ThumbnailProvider.swift new file mode 100644 index 00000000..f3a2c651 --- /dev/null +++ b/Xcode/LockThumbnail/ThumbnailProvider.swift @@ -0,0 +1,99 @@ +// +// ThumbnailProvider.swift +// LockThumbnail +// +// Created by Alsey Coleman Miller on 9/27/22. +// + +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif +import SwiftUI +import QuickLookThumbnailing +import LockKit + +final class ThumbnailProvider: QLThumbnailProvider { + + static let initialize: Void = { + //Log.shared = .thumbnail + log("🖼 Loading \(ThumbnailProvider.self)") + }() + + override func provideThumbnail(for request: QLFileThumbnailRequest, _ handler: @escaping (QLThumbnailReply?, Error?) -> Void) { + + _ = type(of: self).initialize + + log("🖼 Render thumbnail for eKey file \(request.fileURL)") + + #if DEBUG + print(""" + Maximum size: \(request.maximumSize) + Minimum size: \(request.minimumSize) + Scale: \(request.scale) + """) + #endif + + Task(priority: .userInitiated) { + do { + let reply = try await provideThumbnail(for: request) + handler(reply, nil) + } catch { + handler(nil, error) + } + } + } + + private func provideThumbnail(for request: QLFileThumbnailRequest) async throws -> QLThumbnailReply { + + let permission: Permission + let decoder = JSONDecoder() + + do { + let data = try Data(contentsOf: request.fileURL), + invitation = try decoder.decode(NewKey.Invitation.self, from: data) + permission = invitation.key.permission + } catch { + log("⚠️ Unable to load eKey file \(request.fileURL). \(error)") + permission = .admin + } + + let contextWidth = min( + request.maximumSize.width, + request.maximumSize.height + ) + + let contextSize = CGSize( + width: contextWidth, + height: contextWidth + ) + + let imageWidth = contextWidth * 0.8 + + let frame = CGRect( + x: (contextWidth - imageWidth) / 2, + y: (contextWidth - imageWidth) / 2, + width: imageWidth, + height: imageWidth + ) + + log("🖼 Will render \(permission) thumbail \(frame) in \(contextSize)") + + return QLThumbnailReply(contextSize: contextSize, currentContextDrawing: { + + switch permission { + case .owner: + StyleKit.drawPermissionBadgeOwner(frame: frame) + case .admin: + StyleKit.drawPermissionBadgeAdmin(frame: frame) + case .anytime: + StyleKit.drawPermissionBadgeAnytime(frame: frame) + case .scheduled: + StyleKit.drawPermissionBadgeScheduled(frame: frame) + } + + return true + }) + } +} diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index c43ce6eb..74ae05ef 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -97,7 +97,7 @@ 6E5D520328E21538008FFB4D /* PreviewViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6E5D520128E21538008FFB4D /* PreviewViewController.xib */; }; 6E5D520828E21538008FFB4D /* LockQuickLook.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6E5D51F928E21537008FFB4D /* LockQuickLook.appex */; platformFilters = (macos, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 6E5D520C28E215E9008FFB4D /* LockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; }; - 6E5D521328E29825008FFB4D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 6E5D521228E29825008FFB4D /* CodeScanner */; }; + 6E5D521328E29825008FFB4D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; platformFilter = ios; productRef = 6E5D521228E29825008FFB4D /* CodeScanner */; }; 6E6A97EF28DBEC2D00C689F6 /* NewKeyInvitationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97EE28DBEC2D00C689F6 /* NewKeyInvitationStore.swift */; }; 6E6A97F328DC030000C689F6 /* NewKeyInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A97F228DC030000C689F6 /* NewKeyInvitationView.swift */; }; 6E84E52728DDC841008CAE85 /* ScanLocksIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFEC28DD301B00F03735 /* ScanLocksIntent.swift */; }; @@ -120,6 +120,14 @@ 6E84E54E28DF4A98008CAE85 /* MockAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E54A28DF4A98008CAE85 /* MockAttributes.swift */; }; 6E84E54F28DF4A98008CAE85 /* MockScanData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E54B28DF4A98008CAE85 /* MockScanData.swift */; }; 6E84E55128DF59D1008CAE85 /* MockLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E84E55028DF59D1008CAE85 /* MockLock.swift */; }; + 6E857E6928E3566B00EC99D3 /* QuickLookThumbnailing.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E857E6828E3566B00EC99D3 /* QuickLookThumbnailing.framework */; }; + 6E857E6C28E3566B00EC99D3 /* ThumbnailProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E857E6B28E3566B00EC99D3 /* ThumbnailProvider.swift */; }; + 6E857E7028E3566B00EC99D3 /* LockThumbnail.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6E857E6728E3566B00EC99D3 /* LockThumbnail.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 6E857E7428E3589A00EC99D3 /* LockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; }; + 6E857E8728E3652300EC99D3 /* ImportExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E857E8628E3652300EC99D3 /* ImportExtension.swift */; }; + 6E857E8B28E3652300EC99D3 /* LockSpotlight.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6E857E8428E3652300EC99D3 /* LockSpotlight.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 6E857E9028E3656000EC99D3 /* CoreSpotlight.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E857E8F28E3656000EC99D3 /* CoreSpotlight.framework */; }; + 6E857E9128E3656600EC99D3 /* LockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; }; 6E8BBFDA28DC2CA000F03735 /* SetupLockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */; }; 6E8BBFDC28DCC8F300F03735 /* AsyncFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */; }; 6E8BBFEB28DD301B00F03735 /* LockIntentsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFEA28DD301B00F03735 /* LockIntentsExtension.swift */; }; @@ -167,6 +175,34 @@ remoteGlobalIDString = 6EA7769C28D707FE00018FA3; remoteInfo = LockKit; }; + 6E857E6E28E3566B00EC99D3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6EA7767928D7061600018FA3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6E857E6628E3566B00EC99D3; + remoteInfo = LockThumbnail; + }; + 6E857E7628E3589A00EC99D3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6EA7767928D7061600018FA3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6EA7769C28D707FE00018FA3; + remoteInfo = LockKit; + }; + 6E857E8928E3652300EC99D3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6EA7767928D7061600018FA3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6E857E8328E3652300EC99D3; + remoteInfo = LockSpotlight; + }; + 6E857E9328E3656600EC99D3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6EA7767928D7061600018FA3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6EA7769C28D707FE00018FA3; + remoteInfo = LockKit; + }; 6E8BBFF828DD30B400F03735 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 6EA7767928D7061600018FA3 /* Project object */; @@ -192,6 +228,8 @@ files = ( 6E5D520828E21538008FFB4D /* LockQuickLook.appex in Embed Foundation Extensions */, 6E5D51EB28E20C6D008FFB4D /* LockQuickLookMobile.appex in Embed Foundation Extensions */, + 6E857E8B28E3652300EC99D3 /* LockSpotlight.appex in Embed Foundation Extensions */, + 6E857E7028E3566B00EC99D3 /* LockThumbnail.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -318,6 +356,14 @@ 6E84E54A28DF4A98008CAE85 /* MockAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockAttributes.swift; sourceTree = ""; }; 6E84E54B28DF4A98008CAE85 /* MockScanData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockScanData.swift; sourceTree = ""; }; 6E84E55028DF59D1008CAE85 /* MockLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockLock.swift; sourceTree = ""; }; + 6E857E6728E3566B00EC99D3 /* LockThumbnail.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LockThumbnail.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 6E857E6828E3566B00EC99D3 /* QuickLookThumbnailing.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLookThumbnailing.framework; path = System/Library/Frameworks/QuickLookThumbnailing.framework; sourceTree = SDKROOT; }; + 6E857E6B28E3566B00EC99D3 /* ThumbnailProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailProvider.swift; sourceTree = ""; }; + 6E857E6D28E3566B00EC99D3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 6E857E8428E3652300EC99D3 /* LockSpotlight.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LockSpotlight.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 6E857E8628E3652300EC99D3 /* ImportExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportExtension.swift; sourceTree = ""; }; + 6E857E8828E3652300EC99D3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 6E857E8F28E3656000EC99D3 /* CoreSpotlight.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreSpotlight.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/CoreSpotlight.framework; sourceTree = DEVELOPER_DIR; }; 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupLockView.swift; sourceTree = ""; }; 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncFetchRequest.swift; sourceTree = ""; }; 6E8BBFE828DD301B00F03735 /* LockIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.extensionkit-extension"; includeInIndex = 0; path = LockIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -369,6 +415,24 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6E857E6428E3566B00EC99D3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E857E7428E3589A00EC99D3 /* LockKit.framework in Frameworks */, + 6E857E6928E3566B00EC99D3 /* QuickLookThumbnailing.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6E857E8128E3652300EC99D3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E857E9128E3656600EC99D3 /* LockKit.framework in Frameworks */, + 6E857E9028E3656000EC99D3 /* CoreSpotlight.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6E8BBFE528DD301B00F03735 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -643,6 +707,24 @@ path = Mock; sourceTree = ""; }; + 6E857E6A28E3566B00EC99D3 /* LockThumbnail */ = { + isa = PBXGroup; + children = ( + 6E857E6B28E3566B00EC99D3 /* ThumbnailProvider.swift */, + 6E857E6D28E3566B00EC99D3 /* Info.plist */, + ); + path = LockThumbnail; + sourceTree = ""; + }; + 6E857E8528E3652300EC99D3 /* LockSpotlight */ = { + isa = PBXGroup; + children = ( + 6E857E8628E3652300EC99D3 /* ImportExtension.swift */, + 6E857E8828E3652300EC99D3 /* Info.plist */, + ); + path = LockSpotlight; + sourceTree = ""; + }; 6E8BBFE928DD301B00F03735 /* LockIntents */ = { isa = PBXGroup; children = ( @@ -668,6 +750,8 @@ 6E8BBFE928DD301B00F03735 /* LockIntents */, 6E5D51E028E20C6D008FFB4D /* LockQuickLook-iOS */, 6E5D51FC28E21538008FFB4D /* LockQuickLook-macOS */, + 6E857E6A28E3566B00EC99D3 /* LockThumbnail */, + 6E857E8528E3652300EC99D3 /* LockSpotlight */, 6EA7768228D7061600018FA3 /* Products */, 6EA776A928D7082300018FA3 /* Frameworks */, ); @@ -682,6 +766,8 @@ 6E8BBFE828DD301B00F03735 /* LockIntents.appex */, 6E5D51DE28E20C6C008FFB4D /* LockQuickLookMobile.appex */, 6E5D51F928E21537008FFB4D /* LockQuickLook.appex */, + 6E857E6728E3566B00EC99D3 /* LockThumbnail.appex */, + 6E857E8428E3652300EC99D3 /* LockSpotlight.appex */, ); name = Products; sourceTree = ""; @@ -724,9 +810,11 @@ 6EA776A928D7082300018FA3 /* Frameworks */ = { isa = PBXGroup; children = ( + 6E857E8F28E3656000EC99D3 /* CoreSpotlight.framework */, 6E3276C228D70A3700AF171B /* HomeKit.framework */, 6E5D51C128E185DB008FFB4D /* QuickLook.framework */, 6E5D51FA28E21537008FFB4D /* Quartz.framework */, + 6E857E6828E3566B00EC99D3 /* QuickLookThumbnailing.framework */, ); name = Frameworks; sourceTree = ""; @@ -798,6 +886,42 @@ productReference = 6E5D51F928E21537008FFB4D /* LockQuickLook.appex */; productType = "com.apple.product-type.app-extension"; }; + 6E857E6628E3566B00EC99D3 /* LockThumbnail */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6E857E7128E3566B00EC99D3 /* Build configuration list for PBXNativeTarget "LockThumbnail" */; + buildPhases = ( + 6E857E6328E3566B00EC99D3 /* Sources */, + 6E857E6428E3566B00EC99D3 /* Frameworks */, + 6E857E6528E3566B00EC99D3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 6E857E7728E3589A00EC99D3 /* PBXTargetDependency */, + ); + name = LockThumbnail; + productName = LockThumbnail; + productReference = 6E857E6728E3566B00EC99D3 /* LockThumbnail.appex */; + productType = "com.apple.product-type.app-extension"; + }; + 6E857E8328E3652300EC99D3 /* LockSpotlight */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6E857E8C28E3652300EC99D3 /* Build configuration list for PBXNativeTarget "LockSpotlight" */; + buildPhases = ( + 6E857E8028E3652300EC99D3 /* Sources */, + 6E857E8128E3652300EC99D3 /* Frameworks */, + 6E857E8228E3652300EC99D3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 6E857E9428E3656600EC99D3 /* PBXTargetDependency */, + ); + name = LockSpotlight; + productName = LockSpotlight; + productReference = 6E857E8428E3652300EC99D3 /* LockSpotlight.appex */; + productType = "com.apple.product-type.app-extension"; + }; 6E8BBFE728DD301B00F03735 /* LockIntents */ = { isa = PBXNativeTarget; buildConfigurationList = 6E8BBFF228DD301B00F03735 /* Build configuration list for PBXNativeTarget "LockIntents" */; @@ -832,6 +956,8 @@ 6EA776A228D707FE00018FA3 /* PBXTargetDependency */, 6E5D51EA28E20C6D008FFB4D /* PBXTargetDependency */, 6E5D520728E21538008FFB4D /* PBXTargetDependency */, + 6E857E6F28E3566B00EC99D3 /* PBXTargetDependency */, + 6E857E8A28E3652300EC99D3 /* PBXTargetDependency */, ); name = SmartLock; productName = SmartLock; @@ -885,6 +1011,12 @@ 6E5D51F828E21537008FFB4D = { CreatedOnToolsVersion = 14.1; }; + 6E857E6628E3566B00EC99D3 = { + CreatedOnToolsVersion = 14.1; + }; + 6E857E8328E3652300EC99D3 = { + CreatedOnToolsVersion = 14.1; + }; 6E8BBFE728DD301B00F03735 = { CreatedOnToolsVersion = 14.1; }; @@ -924,6 +1056,8 @@ 6E8BBFE728DD301B00F03735 /* LockIntents */, 6E5D51DD28E20C6C008FFB4D /* LockQuickLook-iOS */, 6E5D51F828E21537008FFB4D /* LockQuickLook-macOS */, + 6E857E6628E3566B00EC99D3 /* LockThumbnail */, + 6E857E8328E3652300EC99D3 /* LockSpotlight */, ); }; /* End PBXProject section */ @@ -952,6 +1086,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6E857E6528E3566B00EC99D3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6E857E8228E3652300EC99D3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6E8BBFE628DD301B00F03735 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1003,6 +1151,22 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6E857E6328E3566B00EC99D3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E857E6C28E3566B00EC99D3 /* ThumbnailProvider.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6E857E8028E3652300EC99D3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E857E8728E3652300EC99D3 /* ImportExtension.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6E8BBFE428DD301B00F03735 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1153,6 +1317,26 @@ target = 6EA7769C28D707FE00018FA3 /* LockKit */; targetProxy = 6E5D520E28E215E9008FFB4D /* PBXContainerItemProxy */; }; + 6E857E6F28E3566B00EC99D3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6E857E6628E3566B00EC99D3 /* LockThumbnail */; + targetProxy = 6E857E6E28E3566B00EC99D3 /* PBXContainerItemProxy */; + }; + 6E857E7728E3589A00EC99D3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6EA7769C28D707FE00018FA3 /* LockKit */; + targetProxy = 6E857E7628E3589A00EC99D3 /* PBXContainerItemProxy */; + }; + 6E857E8A28E3652300EC99D3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6E857E8328E3652300EC99D3 /* LockSpotlight */; + targetProxy = 6E857E8928E3652300EC99D3 /* PBXContainerItemProxy */; + }; + 6E857E9428E3656600EC99D3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6EA7769C28D707FE00018FA3 /* LockKit */; + targetProxy = 6E857E9328E3656600EC99D3 /* PBXContainerItemProxy */; + }; 6E8BBFF928DD30B400F03735 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 6EA7769C28D707FE00018FA3 /* LockKit */; @@ -1361,6 +1545,116 @@ }; name = Release; }; + 6E857E7228E3566B00EC99D3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4W79SG34MW; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LockThumbnail/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LockThumbnail; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.Thumbnail; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6E857E7328E3566B00EC99D3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4W79SG34MW; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LockThumbnail/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LockThumbnail; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.Thumbnail; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 6E857E8D28E3652300EC99D3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4W79SG34MW; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LockSpotlight/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LockSpotlight; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.Spotlight; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6E857E8E28E3652300EC99D3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4W79SG34MW; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LockSpotlight/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LockSpotlight; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.Spotlight; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; 6E8BBFF328DD301B00F03735 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1756,6 +2050,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 6E857E7128E3566B00EC99D3 /* Build configuration list for PBXNativeTarget "LockThumbnail" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6E857E7228E3566B00EC99D3 /* Debug */, + 6E857E7328E3566B00EC99D3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6E857E8C28E3652300EC99D3 /* Build configuration list for PBXNativeTarget "LockSpotlight" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6E857E8D28E3652300EC99D3 /* Debug */, + 6E857E8E28E3652300EC99D3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 6E8BBFF228DD301B00F03735 /* Build configuration list for PBXNativeTarget "LockIntents" */ = { isa = XCConfigurationList; buildConfigurations = ( From a982862fdb46572179de4d060238519c69be53cc Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 27 Sep 2022 12:29:54 -0700 Subject: [PATCH 185/229] [App] Added `SpotlightController` --- Xcode/LockKit/Model/AssetExtractor.swift | 3 +- Xcode/LockKit/Model/CoreSpotlight.swift | 172 ++++++++++++++++++++++ Xcode/LockKit/Model/FileManager.swift | 41 +----- Xcode/LockKit/Model/Permission.swift | 29 +++- Xcode/LockKit/Model/Store.swift | 18 ++- Xcode/SmartLock.xcodeproj/project.pbxproj | 162 +------------------- 6 files changed, 224 insertions(+), 201 deletions(-) create mode 100644 Xcode/LockKit/Model/CoreSpotlight.swift diff --git a/Xcode/LockKit/Model/AssetExtractor.swift b/Xcode/LockKit/Model/AssetExtractor.swift index 1bfe1c71..c332a52c 100644 --- a/Xcode/LockKit/Model/AssetExtractor.swift +++ b/Xcode/LockKit/Model/AssetExtractor.swift @@ -14,8 +14,6 @@ import AppKit import SwiftUI /// Get URL from asset. -@MainActor -@available(iOS 8.0, watchOS 6.0, *) public final class AssetExtractor { public static let shared = AssetExtractor() @@ -41,6 +39,7 @@ public final class AssetExtractor { let imageData = image.pngData() else { return nil } #elseif canImport(AppKit) + assert(Thread.isMainThread) let image = Image(imageName, bundle: bundle) let imageRenderer = ImageRenderer(content: image) guard let imageData = imageRenderer.cgImage diff --git a/Xcode/LockKit/Model/CoreSpotlight.swift b/Xcode/LockKit/Model/CoreSpotlight.swift new file mode 100644 index 00000000..67f22d1f --- /dev/null +++ b/Xcode/LockKit/Model/CoreSpotlight.swift @@ -0,0 +1,172 @@ +// +// CoreSpotlight.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 8/21/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +#if canImport(CoreSpotlight) +import Foundation +import CoreSpotlight +import CoreLock + +#if canImport(MobileCoreServices) +import MobileCoreServices +#endif + +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif + +/// Managed the Spotlight index. +public final class SpotlightController { + + // MARK: - Initialization + + public static let shared = SpotlightController(index: .default()) + + private init(index: CSSearchableIndex) { + self.index = index + } + + // MARK: - Properties + + internal let index: CSSearchableIndex + + public var log: ((String) -> ())? = { LockKit.log("🔦 \(SpotlightController.self): " + $0) } + + /// Returns a Boolean value that indicates whether indexing is available on the current device. + public static var isSupported: Bool { + return CSSearchableIndex.isIndexingAvailable() + } + + // MARK: - Methods + + public func reindexAll( + locks: [UUID: LockCache] + ) async throws { + + let searchableItems = locks + .lazy + .map { SearchableLock(id: $0.key, cache: $0.value) } + .map { $0.searchableItem() } + + try await index.deleteSearchableItems(withDomainIdentifiers: [SearchableLock.searchDomain]) + log?("Deleted all old items") + try await index.indexSearchableItems(Array(searchableItems)) + log?("Indexed \(searchableItems.count) items") + } + + /// Reindex the searchable items associated with the specified identifiers. + public func reindex( + _ identifiers: [String], + for locks: [UUID: LockCache] + ) async throws { + + var deletedItems = Set() + var searchableItems = [CSSearchableItem]() + searchableItems.reserveCapacity(identifiers.count) + + for identifier in identifiers { + + guard let viewData = AppActivity.ViewData(rawValue: identifier) else { + log?("⚠️ Invalid index \(identifier)") + continue + } + + switch viewData { + case let .lock(lock): + let searchIdentifier = SearchableLock.searchIdentifier(for: lock) + if let cache = locks[lock] { + let item = SearchableLock(id: lock, cache: cache).searchableItem() + searchableItems.append(item) + } else { + deletedItems.insert(searchIdentifier) + } + } + } + + // delete and reindex certain items + try await index.deleteSearchableItems(withIdentifiers: Array(deletedItems)) + log?("Deleted \(deletedItems.count) old items") + try await index.indexSearchableItems(searchableItems) + log?("Indexed \(searchableItems.count) items") + + } +} + +// MARK: - Supporting Types + +public protocol CoreSpotlightSearchable: AppActivityData { + + static var itemContentType: String { get } + + static var searchDomain: String { get } + + var searchIdentifier: String { get } + + func searchableItem() -> CSSearchableItem + + func searchableAttributeSet() -> CSSearchableItemAttributeSet +} + +public extension CoreSpotlightSearchable { + + static var itemContentType: String { return UTType.text.identifier } + + func searchableItem() -> CSSearchableItem { + let attributeSet = searchableAttributeSet() + return CSSearchableItem( + uniqueIdentifier: searchIdentifier, + domainIdentifier: type(of: self).searchDomain, + attributeSet: attributeSet + ) + } +} + +public struct SearchableLock: Equatable { + + public let id: UUID + + public let cache: LockCache +} + +extension SearchableLock: CoreSpotlightSearchable { + + public static var activityDataType: AppActivity.DataType { return .lock } + + public static var searchDomain: String { return "com.colemancda.Lock.Spotlight.Lock" } + + public var searchIdentifier: String { + return type(of: self).searchIdentifier(for: id) + } + + public static func searchIdentifier(for lock: UUID) -> String { + return AppActivity.ViewData.lock(lock).rawValue + } + + public var appActivity: AppActivity.ViewData { + return .lock(id) + } + + public func searchableAttributeSet() -> CSSearchableItemAttributeSet { + let attributeSet = CSSearchableItemAttributeSet(itemContentType: Swift.type(of: self).itemContentType) + let permission = cache.key.permission + let permissionText = permission.localizedText + attributeSet.displayName = cache.name + attributeSet.contentDescription = permissionText + attributeSet.version = cache.information.version.description + if Thread.isMainThread { + attributeSet.thumbnailURL = AssetExtractor.shared.url(for: permission.type) + } else { + DispatchQueue.main.sync { + attributeSet.thumbnailURL = AssetExtractor.shared.url(for: permission.type) + } + } + return attributeSet + } +} +#endif diff --git a/Xcode/LockKit/Model/FileManager.swift b/Xcode/LockKit/Model/FileManager.swift index b3f69308..7d0dd401 100644 --- a/Xcode/LockKit/Model/FileManager.swift +++ b/Xcode/LockKit/Model/FileManager.swift @@ -79,45 +79,6 @@ public extension FileManager.Lock { get { return read(ApplicationData.self, from: .applicationData) } set { write(newValue, file: .applicationData) } } - - @discardableResult - func save(invitation: NewKey.Invitation) throws -> URL { - - let fileName = "newKey-\(invitation.key.id).ekey" - let data = try jsonEncoder.encode(invitation) - - let fileURL = documentURL.appendingPathComponent(fileName) - guard fileManager.createFile(atPath: fileURL.path, contents: data, attributes: nil) else { - assertionFailure("Could not save file \(fileURL.path)") - throw CocoaError(.fileWriteUnknown) - } - return fileURL - } - - func loadInvitations(invalid: (URL, Error) -> ()) throws -> [URL: NewKey.Invitation] { - - let documents = try fileManager.contentsOfDirectory( - at: documentURL, - includingPropertiesForKeys: nil, - options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants, .skipsPackageDescendants] - ) - - let invitationURLs = documents.filter { $0.pathExtension == NewKey.Invitation.fileExtension } - guard invitationURLs.isEmpty == false - else { return [:] } - - var invitations = [URL: NewKey.Invitation](minimumCapacity: invitationURLs.count) - for url in invitationURLs { - do { - let data = try Data(contentsOf: url, options: .mappedIfSafe) - let invitation = try jsonDecoder.decode(NewKey.Invitation.self, from: data) - invitations[url] = invitation - } catch { - invalid(url, error) - } - } - return invitations - } } private extension FileManager.Lock { @@ -160,7 +121,7 @@ private extension FileManager.Lock { #endif } - log("🗄️ Wrote file \(file.rawValue).json") + log("🗄️ Wrote file \(file.rawValue)") } } diff --git a/Xcode/LockKit/Model/Permission.swift b/Xcode/LockKit/Model/Permission.swift index e5193118..07fc3a85 100644 --- a/Xcode/LockKit/Model/Permission.swift +++ b/Xcode/LockKit/Model/Permission.swift @@ -120,14 +120,23 @@ public extension PermissionType.Image { } } +public extension AssetExtractor { + + /// URL for extracted image of the specified ``PermissionType``. + func url(for permission: PermissionType) -> URL { + let imageName = PermissionType.Image(permissionType: permission) + return self.url(for: imageName.rawValue, in: .lockKit)! + } +} + #if canImport(SwiftUI) import SwiftUI public extension Image { init(permissionType: PermissionType) { - let image = PermissionType.Image(permissionType: permissionType) - self.init(image.rawValue, bundle: .lockKit) + let imageName = PermissionType.Image(permissionType: permissionType) + self.init(imageName.rawValue, bundle: .lockKit) } } #endif @@ -138,8 +147,20 @@ import UIKit public extension UIImage { convenience init(permissionType: PermissionType) { - let image = PermissionType.Image(permissionType: permissionType) - self.init(named: image.rawValue, in: .lockKit, with: nil)! + let imageName = PermissionType.Image(permissionType: permissionType) + self.init(named: imageName.rawValue, in: .lockKit, with: nil)! + } +} +#elseif canImport(AppKit) +import AppKit + +public extension NSImage { + + convenience init(permissionType: PermissionType) { + let imageName = PermissionType.Image(permissionType: permissionType) + let url = Bundle.lockKit.urlForImageResource(imageName.rawValue)! + self.init(contentsOfFile: url.path)! } } + #endif diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift index f483752e..bd0fe50f 100644 --- a/Xcode/LockKit/Model/Store.swift +++ b/Xcode/LockKit/Model/Store.swift @@ -43,6 +43,8 @@ public final class Store: ObservableObject { internal lazy var keychain = Keychain(service: .lock, accessGroup: .lock) + public lazy var spotlight: SpotlightController = .shared + internal lazy var fileManager: FileManager.Lock = .shared public lazy var newKeyInvitations: NewKeyInvitationStore = .shared @@ -184,6 +186,8 @@ public extension Store { private func lockCacheChanged() async { // update CoreData await updateCoreData() + // update Spotlight + await updateSpotlight() // update CloudKit do { try await syncCloud() } catch { log("⚠️ Unable to upload locks to iCloud. \(error)") } @@ -222,6 +226,18 @@ public extension Store { } } +// MARK: - Spotlight + +private extension Store { + + func updateSpotlight() async { + guard SpotlightController.isSupported else { return } + let locks = self.applicationData.locks + do { try await spotlight.reindexAll(locks: locks) } + catch { log("⚠️ Unable to update Spotlight: \(error.localizedDescription)") } + } +} + // MARK: - CoreData internal extension Store { @@ -237,7 +253,7 @@ internal extension Store { // load CoreData let semaphore = DispatchSemaphore(value: 0) persistentContainer.loadPersistentStores { (store, error) in - semaphore.signal() + defer { semaphore.signal() } if let error = error { log("⚠️ Unable to load persistent store: \(error.localizedDescription)") #if DEBUG diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 74ae05ef..22b29d37 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -124,10 +124,8 @@ 6E857E6C28E3566B00EC99D3 /* ThumbnailProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E857E6B28E3566B00EC99D3 /* ThumbnailProvider.swift */; }; 6E857E7028E3566B00EC99D3 /* LockThumbnail.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6E857E6728E3566B00EC99D3 /* LockThumbnail.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 6E857E7428E3589A00EC99D3 /* LockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; }; - 6E857E8728E3652300EC99D3 /* ImportExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E857E8628E3652300EC99D3 /* ImportExtension.swift */; }; - 6E857E8B28E3652300EC99D3 /* LockSpotlight.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6E857E8428E3652300EC99D3 /* LockSpotlight.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 6E857E9028E3656000EC99D3 /* CoreSpotlight.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E857E8F28E3656000EC99D3 /* CoreSpotlight.framework */; }; - 6E857E9128E3656600EC99D3 /* LockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; }; + 6E857E9728E367B400EC99D3 /* CoreSpotlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E857E9628E367B400EC99D3 /* CoreSpotlight.swift */; }; + 6E857E9B28E36ED300EC99D3 /* UserActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E857E9A28E36ED300EC99D3 /* UserActivity.swift */; }; 6E8BBFDA28DC2CA000F03735 /* SetupLockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */; }; 6E8BBFDC28DCC8F300F03735 /* AsyncFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */; }; 6E8BBFEB28DD301B00F03735 /* LockIntentsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8BBFEA28DD301B00F03735 /* LockIntentsExtension.swift */; }; @@ -189,20 +187,6 @@ remoteGlobalIDString = 6EA7769C28D707FE00018FA3; remoteInfo = LockKit; }; - 6E857E8928E3652300EC99D3 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 6EA7767928D7061600018FA3 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 6E857E8328E3652300EC99D3; - remoteInfo = LockSpotlight; - }; - 6E857E9328E3656600EC99D3 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 6EA7767928D7061600018FA3 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 6EA7769C28D707FE00018FA3; - remoteInfo = LockKit; - }; 6E8BBFF828DD30B400F03735 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 6EA7767928D7061600018FA3 /* Project object */; @@ -228,7 +212,6 @@ files = ( 6E5D520828E21538008FFB4D /* LockQuickLook.appex in Embed Foundation Extensions */, 6E5D51EB28E20C6D008FFB4D /* LockQuickLookMobile.appex in Embed Foundation Extensions */, - 6E857E8B28E3652300EC99D3 /* LockSpotlight.appex in Embed Foundation Extensions */, 6E857E7028E3566B00EC99D3 /* LockThumbnail.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; @@ -360,10 +343,9 @@ 6E857E6828E3566B00EC99D3 /* QuickLookThumbnailing.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLookThumbnailing.framework; path = System/Library/Frameworks/QuickLookThumbnailing.framework; sourceTree = SDKROOT; }; 6E857E6B28E3566B00EC99D3 /* ThumbnailProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailProvider.swift; sourceTree = ""; }; 6E857E6D28E3566B00EC99D3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 6E857E8428E3652300EC99D3 /* LockSpotlight.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LockSpotlight.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - 6E857E8628E3652300EC99D3 /* ImportExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportExtension.swift; sourceTree = ""; }; - 6E857E8828E3652300EC99D3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 6E857E8F28E3656000EC99D3 /* CoreSpotlight.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreSpotlight.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/CoreSpotlight.framework; sourceTree = DEVELOPER_DIR; }; + 6E857E9628E367B400EC99D3 /* CoreSpotlight.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreSpotlight.swift; sourceTree = ""; }; + 6E857E9A28E36ED300EC99D3 /* UserActivity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserActivity.swift; sourceTree = ""; }; 6E8BBFD928DC2CA000F03735 /* SetupLockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupLockView.swift; sourceTree = ""; }; 6E8BBFDB28DCC8F300F03735 /* AsyncFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncFetchRequest.swift; sourceTree = ""; }; 6E8BBFE828DD301B00F03735 /* LockIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.extensionkit-extension"; includeInIndex = 0; path = LockIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -424,15 +406,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 6E857E8128E3652300EC99D3 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 6E857E9128E3656600EC99D3 /* LockKit.framework in Frameworks */, - 6E857E9028E3656000EC99D3 /* CoreSpotlight.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 6E8BBFE528DD301B00F03735 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -576,6 +549,8 @@ 6EBD590E28E14A7F00CC3852 /* ScanResultsAsyncDataSource.swift */, 6E84E53528DDE01F008CAE85 /* AssetExtractor.swift */, 6E21830F28D80DCD00A622B3 /* Keychain.swift */, + 6E857E9628E367B400EC99D3 /* CoreSpotlight.swift */, + 6E857E9A28E36ED300EC99D3 /* UserActivity.swift */, 6E21831628D8116C00A622B3 /* NewKey.swift */, 6E21831728D8116C00A622B3 /* NewKeyDocument.swift */, 6E21830628D7D08300A622B3 /* Permission.swift */, @@ -716,15 +691,6 @@ path = LockThumbnail; sourceTree = ""; }; - 6E857E8528E3652300EC99D3 /* LockSpotlight */ = { - isa = PBXGroup; - children = ( - 6E857E8628E3652300EC99D3 /* ImportExtension.swift */, - 6E857E8828E3652300EC99D3 /* Info.plist */, - ); - path = LockSpotlight; - sourceTree = ""; - }; 6E8BBFE928DD301B00F03735 /* LockIntents */ = { isa = PBXGroup; children = ( @@ -751,7 +717,6 @@ 6E5D51E028E20C6D008FFB4D /* LockQuickLook-iOS */, 6E5D51FC28E21538008FFB4D /* LockQuickLook-macOS */, 6E857E6A28E3566B00EC99D3 /* LockThumbnail */, - 6E857E8528E3652300EC99D3 /* LockSpotlight */, 6EA7768228D7061600018FA3 /* Products */, 6EA776A928D7082300018FA3 /* Frameworks */, ); @@ -767,7 +732,6 @@ 6E5D51DE28E20C6C008FFB4D /* LockQuickLookMobile.appex */, 6E5D51F928E21537008FFB4D /* LockQuickLook.appex */, 6E857E6728E3566B00EC99D3 /* LockThumbnail.appex */, - 6E857E8428E3652300EC99D3 /* LockSpotlight.appex */, ); name = Products; sourceTree = ""; @@ -904,24 +868,6 @@ productReference = 6E857E6728E3566B00EC99D3 /* LockThumbnail.appex */; productType = "com.apple.product-type.app-extension"; }; - 6E857E8328E3652300EC99D3 /* LockSpotlight */ = { - isa = PBXNativeTarget; - buildConfigurationList = 6E857E8C28E3652300EC99D3 /* Build configuration list for PBXNativeTarget "LockSpotlight" */; - buildPhases = ( - 6E857E8028E3652300EC99D3 /* Sources */, - 6E857E8128E3652300EC99D3 /* Frameworks */, - 6E857E8228E3652300EC99D3 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 6E857E9428E3656600EC99D3 /* PBXTargetDependency */, - ); - name = LockSpotlight; - productName = LockSpotlight; - productReference = 6E857E8428E3652300EC99D3 /* LockSpotlight.appex */; - productType = "com.apple.product-type.app-extension"; - }; 6E8BBFE728DD301B00F03735 /* LockIntents */ = { isa = PBXNativeTarget; buildConfigurationList = 6E8BBFF228DD301B00F03735 /* Build configuration list for PBXNativeTarget "LockIntents" */; @@ -957,7 +903,6 @@ 6E5D51EA28E20C6D008FFB4D /* PBXTargetDependency */, 6E5D520728E21538008FFB4D /* PBXTargetDependency */, 6E857E6F28E3566B00EC99D3 /* PBXTargetDependency */, - 6E857E8A28E3652300EC99D3 /* PBXTargetDependency */, ); name = SmartLock; productName = SmartLock; @@ -1014,9 +959,6 @@ 6E857E6628E3566B00EC99D3 = { CreatedOnToolsVersion = 14.1; }; - 6E857E8328E3652300EC99D3 = { - CreatedOnToolsVersion = 14.1; - }; 6E8BBFE728DD301B00F03735 = { CreatedOnToolsVersion = 14.1; }; @@ -1057,7 +999,6 @@ 6E5D51DD28E20C6C008FFB4D /* LockQuickLook-iOS */, 6E5D51F828E21537008FFB4D /* LockQuickLook-macOS */, 6E857E6628E3566B00EC99D3 /* LockThumbnail */, - 6E857E8328E3652300EC99D3 /* LockSpotlight */, ); }; /* End PBXProject section */ @@ -1093,13 +1034,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 6E857E8228E3652300EC99D3 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 6E8BBFE628DD301B00F03735 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1159,14 +1093,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 6E857E8028E3652300EC99D3 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 6E857E8728E3652300EC99D3 /* ImportExtension.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 6E8BBFE428DD301B00F03735 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1217,6 +1143,7 @@ 6E21835D28D9516A00A622B3 /* CloudShare.swift in Sources */, 6E21831928D8116C00A622B3 /* NewKeyDocument.swift in Sources */, 6ED248CC28D7A3BF00F78D07 /* ActivityIndicatorView.swift in Sources */, + 6E857E9728E367B400EC99D3 /* CoreSpotlight.swift in Sources */, 6E21830528D7C51900A622B3 /* Log.swift in Sources */, 6E21833B28D8F3B500A622B3 /* ConfirmNewKeyEventManagedObject.swift in Sources */, 6E21830C28D7FEF600A622B3 /* FileManager.swift in Sources */, @@ -1271,6 +1198,7 @@ 6E21836428D9516B00A622B3 /* CloudUser.swift in Sources */, 6E21834E28D91FDC00A622B3 /* Event.swift in Sources */, 6E84E55128DF59D1008CAE85 /* MockLock.swift in Sources */, + 6E857E9B28E36ED300EC99D3 /* UserActivity.swift in Sources */, 6E21830E28D7FF2400A622B3 /* AppGroup.swift in Sources */, 6E21836728D9516B00A622B3 /* CloudNewKey.swift in Sources */, 6E6A97F328DC030000C689F6 /* NewKeyInvitationView.swift in Sources */, @@ -1327,16 +1255,6 @@ target = 6EA7769C28D707FE00018FA3 /* LockKit */; targetProxy = 6E857E7628E3589A00EC99D3 /* PBXContainerItemProxy */; }; - 6E857E8A28E3652300EC99D3 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 6E857E8328E3652300EC99D3 /* LockSpotlight */; - targetProxy = 6E857E8928E3652300EC99D3 /* PBXContainerItemProxy */; - }; - 6E857E9428E3656600EC99D3 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 6EA7769C28D707FE00018FA3 /* LockKit */; - targetProxy = 6E857E9328E3656600EC99D3 /* PBXContainerItemProxy */; - }; 6E8BBFF928DD30B400F03735 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 6EA7769C28D707FE00018FA3 /* LockKit */; @@ -1600,61 +1518,6 @@ }; name = Release; }; - 6E857E8D28E3652300EC99D3 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_IDENTITY = "-"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 4W79SG34MW; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = LockSpotlight/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = LockSpotlight; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.Spotlight; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = auto; - SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; - SWIFT_EMIT_LOC_STRINGS = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 6E857E8E28E3652300EC99D3 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_IDENTITY = "-"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 4W79SG34MW; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = LockSpotlight/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = LockSpotlight; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.Lock.Spotlight; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = auto; - SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; - SWIFT_EMIT_LOC_STRINGS = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; 6E8BBFF328DD301B00F03735 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2059,15 +1922,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 6E857E8C28E3652300EC99D3 /* Build configuration list for PBXNativeTarget "LockSpotlight" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 6E857E8D28E3652300EC99D3 /* Debug */, - 6E857E8E28E3652300EC99D3 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 6E8BBFF228DD301B00F03735 /* Build configuration list for PBXNativeTarget "LockIntents" */ = { isa = XCConfigurationList; buildConfigurations = ( From bc6a4b8ff2df53b87b583d0ccb6a8501e285fba0 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 27 Sep 2022 12:30:39 -0700 Subject: [PATCH 186/229] [App] Added `NSUserActivity` extension --- Xcode/LockKit/Model/UserActivity.swift | 319 +++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 Xcode/LockKit/Model/UserActivity.swift diff --git a/Xcode/LockKit/Model/UserActivity.swift b/Xcode/LockKit/Model/UserActivity.swift new file mode 100644 index 00000000..51e657bc --- /dev/null +++ b/Xcode/LockKit/Model/UserActivity.swift @@ -0,0 +1,319 @@ +// +// UserActivity.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 8/18/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import Intents + +#if canImport(CoreSpotlight) +import CoreSpotlight +#endif + +#if canImport(MobileCoreServices) +import MobileCoreServices +#endif + +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif + +/// `NSUserActivity` type +public enum AppActivityType: String { + + /// App activity for actions. + case action = "com.colemancda.lock.activity.action" + + /// App activity for handoff for data viewing. + case view = "com.colemancda.lock.activity.view" + + /// App activity that takes you to a specific screen (usually with a list of data). + case screen = "com.colemancda.lock.activity.screen" +} + +public enum AppActivity: Equatable, Hashable { + + case screen(Screen) + case view(ViewData) + case action(Action) +} + +public extension AppActivity { + + enum ViewData: Equatable, Hashable { + + case lock(UUID) + } + + enum Action: Equatable, Hashable { + + case shareKey(UUID) + case unlock(UUID) + } + + enum Screen: String { + + case nearbyLocks + case keys + case events + } +} + +public protocol AppActivityData { + + static var activityDataType: AppActivity.DataType { get } +} + +extension LockCache: AppActivityData { + + public static var activityDataType: AppActivity.DataType { return .lock } +} + +public extension AppActivity { + + init?(type: AppActivityType, rawValue: String) { + + switch type { + case .screen: + guard let screen = Screen(rawValue: rawValue) + else { return nil } + self = .screen(screen) + case .action: + guard let action = Action(rawValue: rawValue) + else { return nil } + self = .action(action) + case .view: + guard let viewData = ViewData(rawValue: rawValue) + else { return nil } + self = .view(viewData) + } + } + + var type: AppActivityType { + + switch self { + case .action: return .action + case .screen: return .screen + case .view: return .view + } + } +} + +private extension AppActivity { + + static let separator = "/" +} + +extension AppActivity.ViewData: RawRepresentable { + + public init?(rawValue: String) { + + let subcomponents = rawValue + .components(separatedBy: AppActivity.separator) + .filter { $0.isEmpty == false } + + guard let typeString = subcomponents.first, + let type = AppActivity.DataType(rawValue: typeString) + else { return nil } + + switch type { + case .lock: + guard subcomponents.count == 2, + let uuid = UUID(uuidString: String(subcomponents[1])) + else { return nil } + self = .lock(uuid) + } + } + + public var rawValue: String { + + let components: [String] + + switch self { + case let .lock(identifier): + components = [AppActivity.DataType.lock.rawValue, identifier.uuidString] + } + + return components.reduce("", { $0 + AppActivity.separator + $1 }) + } +} + +extension AppActivity.Action: RawRepresentable { + + public init?(rawValue: String) { + + let subcomponents = rawValue + .components(separatedBy: AppActivity.separator) + .filter { $0.isEmpty == false } + + guard let typeString = subcomponents.first, + let type = AppActivity.ActionType(rawValue: typeString) + else { return nil } + + switch type { + case .unlock: + guard subcomponents.count == 2, + let uuid = UUID(uuidString: String(subcomponents[1])) + else { return nil } + self = .unlock(uuid) + case .shareKey: + guard subcomponents.count == 2, + let uuid = UUID(uuidString: String(subcomponents[1])) + else { return nil } + self = .shareKey(uuid) + } + } + + public var rawValue: String { + + let components: [String] + + switch self { + case let .unlock(identifier): + components = [AppActivity.ActionType.unlock.rawValue, identifier.uuidString] + case let .shareKey(identifier): + components = [AppActivity.ActionType.shareKey.rawValue, identifier.uuidString] + } + + return components.reduce("", { $0 + AppActivity.separator + $1 }) + } +} + +public extension AppActivity { + + enum UserInfo: String { + + case lock + case screen + case action + } + + enum DataType: String { + + case lock + } + + enum ActionType: String { + + case shareKey + case unlock + } +} + +public extension NSUserActivity { + + @MainActor + convenience init(_ activity: AppActivity) { + + switch activity { + case let .screen(screen): + self.init(activityType: .screen, userInfo: [ + .screen: screen.rawValue as NSString + ]) + #if canImport(CoreSpotlight) + let attributes = CSSearchableItemAttributeSet(itemContentType: screen.rawValue) + switch screen { + case .nearbyLocks: + self.title = "Nearby Locks" //R.string.localizable.userActivityNearbyLocksTitle() + attributes.contentDescription = "Nearby Locks" //R.string.localizable.userActivityNearbyLocksDescription() + //attributes.thumbnailData = UIImage(lockKit: "activityNear")?.pngData() + case .keys: + self.title = "Keys" //R.string.localizable.userActivityKeysTitle() + attributes.contentDescription = "Keys" //R.string.localizable.userActivityKeysDescription() + //attributes.thumbnailData = UIImage(lockKit: "activityLock")?.pngData() + case .events: + self.title = "History" //R.string.localizable.userActivityEventsTitle() + attributes.contentDescription = "Lock events" //R.string.localizable.userActivityEventsDescription() + } + self.contentAttributeSet = attributes + #endif + self.isEligibleForSearch = false + self.isEligibleForHandoff = true + self.isEligibleForPublicIndexing = true // show in Siri Shortcuts gallery + #if os(iOS) || os(watchOS) + self.isEligibleForPrediction = true + #endif + case let .view(.lock(lockIdentifier)): + self.init(activityType: .view, userInfo: [ + .lock: lockIdentifier.uuidString as NSString + ]) + if let lockCache = Store.shared.applicationData.locks[lockIdentifier] { + self.title = lockCache.name + #if canImport(CoreSpotlight) + self.contentAttributeSet = SearchableLock(id: lockIdentifier, cache: lockCache).searchableAttributeSet() + #endif + } else { + self.title = "Lock \(lockIdentifier)" + } + self.isEligibleForSearch = true // use Spotlight instead + self.isEligibleForHandoff = true + self.isEligibleForPublicIndexing = true // show in Siri Shortcuts gallery, + #if os(iOS) || os(watchOS) + self.isEligibleForPrediction = false + #endif + case let .action(.shareKey(lockIdentifier)): + self.init(activityType: .action, userInfo: [ + .action: AppActivity.ActionType.shareKey.rawValue as NSString, + .lock: lockIdentifier.uuidString as NSString + ]) + #if canImport(CoreSpotlight) + let attributes = CSSearchableItemAttributeSet(itemContentType: UTType.text.identifier) + //attributes.thumbnailData = UIImage(lockKit: "activityNewKey")?.pngData() + self.contentAttributeSet = attributes + #endif + if let lockCache = Store.shared[lock: lockIdentifier] { + self.title = "Share Key for \(lockCache.name)" + } else { + self.title = "Share Key for \(lockIdentifier)" + } + self.isEligibleForSearch = false + self.isEligibleForHandoff = false + #if os(iOS) || os(watchOS) + self.isEligibleForPrediction = true + #endif + case let .action(.unlock(lockIdentifier)): + self.init(activityType: .action, userInfo: [ + .action: AppActivity.ActionType.unlock.rawValue as NSString, + .lock: lockIdentifier.uuidString as NSString + ]) + if let lockCache = Store.shared[lock: lockIdentifier] { + self.title = "Unlock \"\(lockCache.name)\"" + } else { + self.title = "Unlock \(lockIdentifier)" + } + self.isEligibleForSearch = false + self.isEligibleForHandoff = false + #if os(iOS) || os(watchOS) + if #available(iOS 12.0, watchOS 5.0, *) { + self.isEligibleForPrediction = true + self.suggestedInvocationPhrase = "Unlock my door" + } + #endif + } + + switch activity { + case let .action(action): + self.persistentIdentifier = action.rawValue + case let .screen(screen): + self.persistentIdentifier = screen.rawValue + case let .view(view): + self.persistentIdentifier = view.rawValue + } + } + + private convenience init(activityType: AppActivityType, userInfo: [AppActivity.UserInfo: NSObject]) { + + self.init(activityType: activityType.rawValue) + var data = [String: Any](minimumCapacity: userInfo.count) + for (key, value) in userInfo { + data[key.rawValue] = value + } + self.userInfo = data + self.requiredUserInfoKeys = Set(userInfo.keys.lazy.map { $0.rawValue }) + } +} From f57c2458ca7f05e0158a2f28a9875212dddb2345 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 27 Sep 2022 13:32:03 -0700 Subject: [PATCH 187/229] [App] Fixed `AssetExtractor` for macOS --- Xcode/LockKit/Model/AssetExtractor.swift | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Xcode/LockKit/Model/AssetExtractor.swift b/Xcode/LockKit/Model/AssetExtractor.swift index c332a52c..73b491d8 100644 --- a/Xcode/LockKit/Model/AssetExtractor.swift +++ b/Xcode/LockKit/Model/AssetExtractor.swift @@ -29,27 +29,21 @@ public final class AssetExtractor { }() public func url(for imageName: String, in bundle: Bundle = .lockKit) -> URL? { - let fileName = (bundle.bundleIdentifier ?? bundle.bundleURL.lastPathComponent) + "." + imageName + ".png" let url = cachesDirectory.appendingPathComponent(fileName) - if fileManager.fileExists(atPath: url.path) == false { #if canImport(UIKit) guard let image = UIImage(named: imageName, in: bundle, compatibleWith: nil), let imageData = image.pngData() else { return nil } #elseif canImport(AppKit) - assert(Thread.isMainThread) - let image = Image(imageName, bundle: bundle) - let imageRenderer = ImageRenderer(content: image) - guard let imageData = imageRenderer.cgImage - .map({ NSBitmapImageRep(cgImage: $0) })? - .representation(using: .png, properties: [:]) + guard let image = bundle.image(forResource: imageName), + let bitmap = image.tiffRepresentation.flatMap({ NSBitmapImageRep(data: $0) }), + let imageData = bitmap.representation(using: .png, properties: [:]) else { return nil } #endif fileManager.createFile(atPath: url.path, contents: imageData, attributes: nil) } - return url } } From f5f938c03f94d0307acaa976565ab43acff1261a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 27 Sep 2022 19:19:51 -0700 Subject: [PATCH 188/229] [App] Added iMessage extension --- .../LockMessage/Assets.xcassets/Contents.json | 6 + .../Contents.json | 91 +++++++ .../icon-27x20@2x.png | Bin 0 -> 2801 bytes .../icon-27x20@3x.png | Bin 0 -> 4830 bytes .../icon-29x29@2x.png | Bin 0 -> 3499 bytes .../icon-29x29@3x.png | Bin 0 -> 5418 bytes .../icon-29x29~ipad@2x.png | Bin 0 -> 3499 bytes .../icon-32x24@2x.png | Bin 0 -> 3262 bytes .../icon-32x24@3x.png | Bin 0 -> 4916 bytes .../icon-60x45@2x.png | Bin 0 -> 6335 bytes .../icon-60x45@3x.png | Bin 0 -> 10935 bytes .../icon-67x50~ipad@2x.png | Bin 0 -> 6976 bytes .../icon-74x55~ipad@2x.png | Bin 0 -> 7852 bytes .../marketing-1024x1024.png | Bin 0 -> 75896 bytes .../marketing-1024x768.png | Bin 0 -> 61230 bytes .../Base.lproj/MainInterface.storyboard | 64 +++++ Xcode/LockMessage/Info.plist | 13 + Xcode/LockMessage/LockMessage.entitlements | 24 ++ Xcode/LockMessage/MessagesView.swift | 79 ++++++ .../LockMessage/MessagesViewController.swift | 230 ++++++++++++++++++ Xcode/SmartLock.xcodeproj/project.pbxproj | 230 ++++++++++++++++++ 21 files changed, 737 insertions(+) create mode 100644 Xcode/LockMessage/Assets.xcassets/Contents.json create mode 100644 Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/Contents.json create mode 100644 Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-27x20@2x.png create mode 100644 Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-27x20@3x.png create mode 100644 Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-29x29@2x.png create mode 100644 Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-29x29@3x.png create mode 100644 Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-29x29~ipad@2x.png create mode 100644 Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-32x24@2x.png create mode 100644 Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-32x24@3x.png create mode 100644 Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-60x45@2x.png create mode 100644 Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-60x45@3x.png create mode 100644 Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-67x50~ipad@2x.png create mode 100644 Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-74x55~ipad@2x.png create mode 100644 Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/marketing-1024x1024.png create mode 100644 Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/marketing-1024x768.png create mode 100644 Xcode/LockMessage/Base.lproj/MainInterface.storyboard create mode 100644 Xcode/LockMessage/Info.plist create mode 100644 Xcode/LockMessage/LockMessage.entitlements create mode 100644 Xcode/LockMessage/MessagesView.swift create mode 100644 Xcode/LockMessage/MessagesViewController.swift diff --git a/Xcode/LockMessage/Assets.xcassets/Contents.json b/Xcode/LockMessage/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Xcode/LockMessage/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/Contents.json b/Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/Contents.json new file mode 100644 index 00000000..f2752e2d --- /dev/null +++ b/Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/Contents.json @@ -0,0 +1,91 @@ +{ + "images" : [ + { + "filename" : "icon-29x29@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "icon-29x29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "icon-60x45@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x45" + }, + { + "filename" : "icon-60x45@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x45" + }, + { + "filename" : "icon-29x29~ipad@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "icon-67x50~ipad@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "67x50" + }, + { + "filename" : "icon-74x55~ipad@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "74x55" + }, + { + "filename" : "marketing-1024x1024.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "filename" : "icon-27x20@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "27x20" + }, + { + "filename" : "icon-27x20@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "27x20" + }, + { + "filename" : "icon-32x24@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "32x24" + }, + { + "filename" : "icon-32x24@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "32x24" + }, + { + "filename" : "marketing-1024x768.png", + "idiom" : "ios-marketing", + "platform" : "ios", + "scale" : "1x", + "size" : "1024x768" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-27x20@2x.png b/Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-27x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8a0f54fe6c8a19c99517de481add67374271fb6d GIT binary patch literal 2801 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 Ef~J~bkpKVy literal 0 HcmV?d00001 diff --git a/Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-29x29@2x.png b/Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1358ac5fcf61bf7b25f0b9204427da37efe2047b GIT binary patch literal 3499 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 Z@BcOmyWzl*QVRe8002ovPDHLkV1kyKjZgpp literal 0 HcmV?d00001 diff --git a/Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-29x29@3x.png b/Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-29x29@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..ac965e49981cf0b14ed2f3a36574edd82e3947cc GIT binary patch literal 5418 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 U0@_JQNB{r;07*qoM6N<$f{PMfx&QzG literal 0 HcmV?d00001 diff --git a/Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-29x29~ipad@2x.png b/Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-29x29~ipad@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1358ac5fcf61bf7b25f0b9204427da37efe2047b GIT binary patch literal 3499 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 Z@BcOmyWzl*QVRe8002ovPDHLkV1kyKjZgpp literal 0 HcmV?d00001 diff --git a/Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-32x24@2x.png b/Xcode/LockMessage/Assets.xcassets/iMessage App Icon.stickersiconset/icon-32x24@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..929a01f08f26723609e03c051464707d1df28c75 GIT binary patch literal 3262 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þ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 dh4Z*D^uK>a9PodCJ~re3{`~*HNA|n&e*lMwmJ Date: Fri, 16 Sep 2022 22:46:54 -0700 Subject: [PATCH 004/229] Fixed Date encoding --- Sources/CoreLock/Bluetooth/TLV.swift | 3 ++ Sources/CoreLock/Crypto/Authentication.swift | 2 +- Sources/CoreLock/Extensions/Date.swift | 15 +++++++ Sources/CoreLock/LockAuthorizationStore.swift | 2 +- Tests/CoreLockTests/GATTProfileTests.swift | 40 +++++++++++-------- 5 files changed, 44 insertions(+), 18 deletions(-) create mode 100644 Sources/CoreLock/Extensions/Date.swift diff --git a/Sources/CoreLock/Bluetooth/TLV.swift b/Sources/CoreLock/Bluetooth/TLV.swift index 6135ad23..9abae795 100644 --- a/Sources/CoreLock/Bluetooth/TLV.swift +++ b/Sources/CoreLock/Bluetooth/TLV.swift @@ -14,6 +14,7 @@ internal extension TLVEncoder { var encoder = TLVEncoder() encoder.numericFormatting = .littleEndian encoder.uuidFormatting = .bytes + encoder.dateFormatting = .secondsSince1970 return encoder } } @@ -24,6 +25,7 @@ internal extension TLVDecoder { var decoder = TLVDecoder() decoder.numericFormatting = .littleEndian decoder.uuidFormatting = .bytes + decoder.dateFormatting = .secondsSince1970 return decoder } } @@ -42,6 +44,7 @@ public protocol TLVCharacteristic: GATTProfileCharacteristic { public extension TLVCharacteristic { static var encoder: TLVEncoder { return .lock } + static var decoder: TLVDecoder { return .lock } } diff --git a/Sources/CoreLock/Crypto/Authentication.swift b/Sources/CoreLock/Crypto/Authentication.swift index 88261ca0..5d7fbd64 100644 --- a/Sources/CoreLock/Crypto/Authentication.swift +++ b/Sources/CoreLock/Crypto/Authentication.swift @@ -42,7 +42,7 @@ public struct AuthenticationMessage: Equatable, Codable { digest: Digest, id: UUID ) { - self.date = date + self.date = date.removingMiliseconds self.nonce = nonce self.digest = digest self.id = id diff --git a/Sources/CoreLock/Extensions/Date.swift b/Sources/CoreLock/Extensions/Date.swift new file mode 100644 index 00000000..46dc55d7 --- /dev/null +++ b/Sources/CoreLock/Extensions/Date.swift @@ -0,0 +1,15 @@ +// +// Date.swift +// +// +// Created by Alsey Coleman Miller on 9/16/22. +// + +import Foundation + +internal extension Date { + + var removingMiliseconds: Date { + Date(timeIntervalSinceReferenceDate: Double(Int(self.timeIntervalSinceReferenceDate))) + } +} diff --git a/Sources/CoreLock/LockAuthorizationStore.swift b/Sources/CoreLock/LockAuthorizationStore.swift index 7eb5a98b..1004aada 100644 --- a/Sources/CoreLock/LockAuthorizationStore.swift +++ b/Sources/CoreLock/LockAuthorizationStore.swift @@ -8,7 +8,7 @@ import Foundation /// Lock Authorization Store -public protocol LockAuthorizationStore: class { +public protocol LockAuthorizationStore: AnyObject { var isEmpty: Bool { get } diff --git a/Tests/CoreLockTests/GATTProfileTests.swift b/Tests/CoreLockTests/GATTProfileTests.swift index 8ab092f7..23fef2d1 100644 --- a/Tests/CoreLockTests/GATTProfileTests.swift +++ b/Tests/CoreLockTests/GATTProfileTests.swift @@ -26,23 +26,31 @@ final class GATTProfileTests: XCTestCase { XCTAssertEqual(information, decoded) } - func testUnlock() throws { + func testUnlock() async throws { - let key = (id: UUID(), secret: KeyData()) - let request = UnlockRequest(action: .default) - let characteristic = try UnlockCharacteristic(request: request, using: key.secret, id: key.id) - guard let decodedCharacteristic = UnlockCharacteristic(data: characteristic.data) - else { XCTFail("Could not parse bytes"); return } - let decodedRequest = try decodedCharacteristic.decrypt(with: key.secret) - XCTAssertEqual(decodedRequest, request) - XCTAssertEqual(characteristic, decodedCharacteristic) - XCTAssertEqual(characteristic.encryptedData, decodedCharacteristic.encryptedData) - XCTAssertEqual(characteristic.encryptedData.authentication, decodedCharacteristic.encryptedData.authentication) - XCTAssertEqual(characteristic.encryptedData.authentication.message, decodedCharacteristic.encryptedData.authentication.message) - XCTAssertEqual(characteristic.encryptedData.authentication.signedData, decodedCharacteristic.encryptedData.authentication.signedData) - XCTAssert(decodedCharacteristic.encryptedData.authentication.isAuthenticated(using: key.secret)) - XCTAssert(characteristic.encryptedData.authentication.isAuthenticated(using: key.secret)) - //XCTAssertFalse(Authentication(key: key.secret, message: characteristic.encryptedData.authentication.message).isAuthenticated(using: key.secret)) + for _ in 0 ..< 20 { + + let key = (id: UUID(), secret: KeyData()) + let request = UnlockRequest(action: .default) + let characteristic = try UnlockCharacteristic(request: request, using: key.secret, id: key.id) + guard let decodedCharacteristic = UnlockCharacteristic(data: characteristic.data) + else { XCTFail("Could not parse bytes"); return } + let decodedRequest = try decodedCharacteristic.decrypt(with: key.secret) + XCTAssertEqual(decodedRequest, request) + XCTAssertEqual(characteristic, decodedCharacteristic) + XCTAssertEqual(characteristic.encryptedData.encryptedData, decodedCharacteristic.encryptedData.encryptedData) + XCTAssertEqual(characteristic.encryptedData.authentication, decodedCharacteristic.encryptedData.authentication) + XCTAssertEqual(characteristic.encryptedData.authentication.message, decodedCharacteristic.encryptedData.authentication.message) + XCTAssertEqual(characteristic.encryptedData.authentication.message.nonce, decodedCharacteristic.encryptedData.authentication.message.nonce) + XCTAssertEqual(characteristic.encryptedData.authentication.message.date, decodedCharacteristic.encryptedData.authentication.message.date) + XCTAssertEqual(characteristic.encryptedData.authentication.message.id, decodedCharacteristic.encryptedData.authentication.message.id) + XCTAssertEqual(characteristic.encryptedData.authentication.signedData, decodedCharacteristic.encryptedData.authentication.signedData) + XCTAssert(decodedCharacteristic.encryptedData.authentication.isAuthenticated(using: key.secret)) + XCTAssert(characteristic.encryptedData.authentication.isAuthenticated(using: key.secret)) + XCTAssert(Authentication(key: key.secret, message: characteristic.encryptedData.authentication.message).isAuthenticated(using: key.secret)) + + try await Task.sleep(nanoseconds: 100_000_000) + } } func testSetup() throws { From 3623bc34b13f158fd531d6219825397f2c1545fd Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 16 Sep 2022 22:51:46 -0700 Subject: [PATCH 005/229] Added GitHub CI --- .github/workflows/swift.yml | 61 +++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .github/workflows/swift.yml diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml new file mode 100644 index 00000000..3f074206 --- /dev/null +++ b/.github/workflows/swift.yml @@ -0,0 +1,61 @@ +name: Swift + +on: [push] + +jobs: + build: + name: Build + strategy: + matrix: + swift: [5.6.3, 5.7] + os: [ubuntu-20.04, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Install Swift + uses: slashmo/install-swift@v0.3.0 + with: + version: ${{ matrix.swift }} + - name: Checkout + uses: actions/checkout@v2 + - name: Swift Version + run: swift --version + - name: Build (Debug) + run: swift build -c debug + - name: Build (Release) + run: swift build -c release + + test-linux: + name: Test Linux + strategy: + matrix: + swift: [5.6.3, 5.7] + os: [ubuntu-20.04] + runs-on: ${{ matrix.os }} + steps: + - name: Install Swift + uses: slashmo/install-swift@v0.3.0 + with: + version: ${{ matrix.swift }} + - name: Checkout + uses: actions/checkout@v2 + - name: Swift Version + run: swift --version + - name: Test (Debug) + run: swift test --configuration debug --enable-code-coverage + - name: Test (Release) + run: swift test --configuration release -Xswiftc -enable-testing --enable-code-coverage + - name: Coverage Report + uses: maxep/spm-lcov-action@0.3.1 + + test-macOS: + name: Test macOS + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Swift Version + run: swift --version + - name: Test (Debug) + run: swift test --configuration debug --enable-code-coverage + - name: Test (Release) + run: swift test --configuration release -Xswiftc -enable-testing --enable-code-coverage From 3ddab41f74805cd4673b7898d0df3d73a1515919 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 16 Sep 2022 22:52:01 -0700 Subject: [PATCH 006/229] Added FUNDING --- .github/FUNDING.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..dc74f984 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +# These are supported funding model platforms +ko_fi: colemancda From bce40162674794222e17339ee2b909922bcae964 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 16 Sep 2022 22:52:09 -0700 Subject: [PATCH 007/229] Added GitHub PR template --- .github/pull_request_template.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..07929ebd --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +**Issue** + +Fixes #1. + +**What does this PR Do?** + +Description of the changes in this pull request. + +**Where should the reviewer start?** + +`main.swift` + +**Sweet giphy showing how you feel about this PR** + +![Giphy](https://media.giphy.com/media/rkDXJA9GoWR2/giphy.gif) From f9fba3cfd12a64152892bb217c475ce5029a518d Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 16 Sep 2022 22:56:50 -0700 Subject: [PATCH 008/229] Updated GitHub CI --- .github/workflows/swift.yml | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 3f074206..52abe131 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -24,12 +24,12 @@ jobs: - name: Build (Release) run: swift build -c release - test-linux: - name: Test Linux + test: + name: Test strategy: matrix: swift: [5.6.3, 5.7] - os: [ubuntu-20.04] + os: [ubuntu-20.04, macos-latest] runs-on: ${{ matrix.os }} steps: - name: Install Swift @@ -46,16 +46,3 @@ jobs: run: swift test --configuration release -Xswiftc -enable-testing --enable-code-coverage - name: Coverage Report uses: maxep/spm-lcov-action@0.3.1 - - test-macOS: - name: Test macOS - runs-on: macos-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Swift Version - run: swift --version - - name: Test (Debug) - run: swift test --configuration debug --enable-code-coverage - - name: Test (Release) - run: swift test --configuration release -Xswiftc -enable-testing --enable-code-coverage From 40f90685288a35e5a1bf903f7ce4a6a8a353a072 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 16 Sep 2022 22:58:25 -0700 Subject: [PATCH 009/229] Fixed Linux support --- Tests/CoreLockTests/URLTests.swift | 4 ---- Tests/LinuxMain.swift | 7 ------- 2 files changed, 11 deletions(-) delete mode 100644 Tests/LinuxMain.swift diff --git a/Tests/CoreLockTests/URLTests.swift b/Tests/CoreLockTests/URLTests.swift index 754ecae7..dbf97b73 100644 --- a/Tests/CoreLockTests/URLTests.swift +++ b/Tests/CoreLockTests/URLTests.swift @@ -12,10 +12,6 @@ import XCTest final class URLTests: XCTestCase { - static let allTests = [ - ("testSetup", testSetup) - ] - func testSetup() { let url = URL(string: "lock:/setup/25261345-ADC6-4802-882B-613AD8E86BE1/AcDIoBrCWorulJh4WBRr2z0KTWxzXt9Rz37bOqHYChA=")! diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index aa95426d..00000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XCTest -@testable import CoreLockTests - -XCTMain([ - testCase(CryptoTests.allTests), - testCase(GATTProfileTests.allTests), -]) From a0826399941b5f691dd0449f294164820b15496b Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 16 Sep 2022 23:09:02 -0700 Subject: [PATCH 010/229] Updated GitHub CI --- .github/workflows/swift.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 52abe131..dc6f84fd 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -41,8 +41,4 @@ jobs: - name: Swift Version run: swift --version - name: Test (Debug) - run: swift test --configuration debug --enable-code-coverage - - name: Test (Release) - run: swift test --configuration release -Xswiftc -enable-testing --enable-code-coverage - - name: Coverage Report - uses: maxep/spm-lcov-action@0.3.1 + run: swift test --configuration debug From fd44433ff41575d80add31b413571270b969e30a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 16 Sep 2022 23:34:08 -0700 Subject: [PATCH 011/229] Updated GitHub CI --- .github/workflows/swift.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index dc6f84fd..61d63149 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -28,7 +28,7 @@ jobs: name: Test strategy: matrix: - swift: [5.6.3, 5.7] + swift: [5.7] os: [ubuntu-20.04, macos-latest] runs-on: ${{ matrix.os }} steps: From c57ef6a107cc30114ba2c27a7ddec4b13f9dc6cd Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 16 Sep 2022 23:38:18 -0700 Subject: [PATCH 012/229] Added `LockInformation` --- Sources/CoreLock/LockInformation.swift | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 Sources/CoreLock/LockInformation.swift diff --git a/Sources/CoreLock/LockInformation.swift b/Sources/CoreLock/LockInformation.swift new file mode 100644 index 00000000..752ec865 --- /dev/null +++ b/Sources/CoreLock/LockInformation.swift @@ -0,0 +1,39 @@ +// +// LockInformation.swift +// +// +// Created by Alsey Coleman Miller on 9/16/22. +// + +import Foundation + +public struct LockInformation: Equatable, Hashable, Codable { + + /// Lock identifier + public let id: 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(id: UUID, + buildVersion: LockBuildVersion = .current, + version: LockVersion = .current, + status: LockStatus, + unlockActions: Set = [.default]) { + + self.id = id + self.buildVersion = buildVersion + self.version = version + self.status = status + self.unlockActions = unlockActions + } +} From 5810dca3a86ed12647c010fe8eb801c8766f9949 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 16 Sep 2022 23:38:37 -0700 Subject: [PATCH 013/229] Added `CentralManager` extensions --- Sources/CoreLock/Bluetooth/Central.swift | 294 ++++++++++++++--------- 1 file changed, 178 insertions(+), 116 deletions(-) diff --git a/Sources/CoreLock/Bluetooth/Central.swift b/Sources/CoreLock/Bluetooth/Central.swift index 230fb4d2..610d5807 100644 --- a/Sources/CoreLock/Bluetooth/Central.swift +++ b/Sources/CoreLock/Bluetooth/Central.swift @@ -8,157 +8,219 @@ import Foundation import Bluetooth import GATT -/* -internal extension CentralProtocol { + +public extension CentralManager { - /// Connects to the device, fetches the data, performs the action, and disconnects. - func device (for peripheral: Peripheral, - timeout: Timeout, - _ action: (GATTConnectionCache) throws -> (T)) throws -> T { - + func connection( + for peripheral: Peripheral, + _ connection: (GATTConnection + ) async throws -> (T)) async throws -> T { + // connect first - try connect(to: peripheral, timeout: try timeout.timeRemaining()) - - // disconnect - defer { disconnect(peripheral: peripheral) } - - var cache = GATTConnectionCache(peripheral: peripheral) - - let foundServices = try discoverServices([], for: peripheral, timeout: try timeout.timeRemaining()) + try await self.connect(to: peripheral) - for service in foundServices { + do { + // cache MTU + let maximumTransmissionUnit = try await self.maximumTransmissionUnit(for: peripheral) - // validate characteristic exists - let foundCharacteristics = try discoverCharacteristics([], for: service, timeout: try timeout.timeRemaining()) + // get characteristics by UUID + let characteristics = try await self.characteristics(for: peripheral) + + let cache = GATTConnection( + central: self, + maximumTransmissionUnit: maximumTransmissionUnit, + characteristics: characteristics + ) - cache.characteristics += foundCharacteristics + // perform action + let value = try await connection(cache) + // disconnect + await self.disconnect(peripheral) + return value + } + catch { + await self.disconnect(peripheral) + throw error } - - // perform action - return try action(cache) } +} + +public struct GATTConnection { - func write (_ characteristic: T, - for cache: GATTConnectionCache, - withResponse response: Bool = true, - timeout: Timeout) throws { + internal let central: Central - guard let foundCharacteristic = cache.characteristics.first(where: { $0.uuid == T.uuid }) - else { throw CentralError.invalidAttribute(T.uuid) } - - try self.writeValue(characteristic.data, - for: foundCharacteristic, - withResponse: response, - timeout: try timeout.timeRemaining()) + public let maximumTransmissionUnit: GATT.MaximumTransmissionUnit + + internal let characteristics: [BluetoothUUID: [Characteristic]] +} + +public extension GATTConnection { + + subscript (type: GATTProfileCharacteristic.Type) -> Characteristic? { + return try? characteristic(for: type) } - func read (_ characteristic: T.Type, - for cache: GATTConnectionCache, - timeout: Timeout) throws -> T { - - guard let foundCharacteristic = cache.characteristics.first(where: { $0.uuid == T.uuid }) - else { throw CentralError.invalidAttribute(T.uuid) } - - let data = try self.readValue(for: foundCharacteristic, - timeout: try timeout.timeRemaining()) - - guard let value = T.init(data: data) - else { throw GATTError.invalidData(data) } - - return value + func characteristic(for type: GATTProfileCharacteristic.Type) throws -> Characteristic { + guard let cache = self.characteristics[type.service.uuid] + else { throw GATTError.serviceNotFound(type.service.uuid) } + guard let foundCharacteristic = cache.first(where: { $0.uuid == type.uuid }) + else { throw GATTError.characteristicNotFound(type.uuid) } + return foundCharacteristic + } + + func read(_ type: T.Type) async throws -> T { + let characteristics = self.characteristics[T.service.uuid] ?? [] + return try await central.read(type, for: characteristics) + } + + func write(_ value: T, response: Bool = true) async throws { + let characteristics = self.characteristics[T.service.uuid] ?? [] + try await central.write(value, for: characteristics, response: response) + } + + func notify( + _ type: T.Type + ) async throws -> AsyncIndefiniteStream { + let characteristics = self.characteristics[T.service.uuid] ?? [] + return try await central.notify(type, for: characteristics) + } +} + +internal extension CentralManager { + + /// Connects to the device, fetches the data, and performs the action, and disconnects. + func connection( + for peripheral: Peripheral, + characteristics: [GATTProfileCharacteristic.Type], + _ action: ([Characteristic] + ) throws -> (T)) async throws -> T { + + // connect first + try await self.connect(to: peripheral) + + do { + // get characteristics by UUID + let foundCharacteristics = try await self.characteristics( + characteristics, + for: peripheral + ) + + // perform action + let value = try action(foundCharacteristics) + // disconnect + await self.disconnect(peripheral) + return value + } + catch { + await self.disconnect(peripheral) + throw error + } } - func notify (_ characteristic: T.Type, - for cache: GATTConnectionCache, - timeout: Timeout, - notification: ((ErrorValue) -> ())?) throws { + /// Verify a peripheral declares the GATT profile. + func characteristics( + _ characteristics: [GATTProfileCharacteristic.Type], + for peripheral: Peripheral + ) async throws -> [Characteristic] { + + // group characteristics by service + var characteristicsByService = [BluetoothUUID: [BluetoothUUID]]() + characteristics.forEach { + characteristicsByService[$0.service.uuid] = (characteristicsByService[$0.service.uuid] ?? []) + [$0.uuid] + } - guard let foundCharacteristic = cache.characteristics.first(where: { $0.uuid == T.uuid }) - else { throw CentralError.invalidAttribute(T.uuid) } + var results = [Characteristic]() - let dataNotification: ((Data) -> ())? + // validate required characteristics + let foundServices = try await discoverServices([], for: peripheral) - if let notification = notification { + for (serviceUUID, characteristics) in characteristicsByService { - dataNotification = { (data) in - - let response: ErrorValue - - if let value = T.init(data: data) { - - response = .value(value) - - } else { - - response = .error(LockGATTError.invalidData(data)) - } + // validate service exists + guard let service = foundServices.first(where: { $0.uuid == serviceUUID }) + else { throw GATTError.serviceNotFound(serviceUUID) } + + // validate characteristic exists + let foundCharacteristics = try await discoverCharacteristics([], for: service) + + for characteristicUUID in characteristics { - notification(response) + guard foundCharacteristics.contains(where: { $0.uuid == characteristicUUID }) + else { throw GATTError.characteristicNotFound(characteristicUUID) } } - } else { - - dataNotification = nil + results += foundCharacteristics } - try notify(dataNotification, for: foundCharacteristic, timeout: try timeout.timeRemaining()) + return results } -} - -// MARK: - Supporting Types - -/// Basic wrapper for error / value pairs. -internal enum ErrorValue { - - case error(Error) - case value(T) -} - -internal struct GATTConnectionCache { - let peripheral: Peripheral - - fileprivate(set) var characteristics: [Characteristic] - - fileprivate init(peripheral: Peripheral) { + /// Fetch all characteristics for all services. + func characteristics( + for peripheral: Peripheral + ) async throws -> [BluetoothUUID: [Characteristic]] { - self.peripheral = peripheral - self.characteristics = [] + var characteristicsByService = [BluetoothUUID: [Characteristic]]() + let foundServices = try await discoverServices([], for: peripheral) + for service in foundServices { + let foundCharacteristics = try await discoverCharacteristics([], for: service) + for characteristic in foundCharacteristics { + characteristicsByService[service.uuid, default: []] + .append(characteristic) + } + } + return characteristicsByService } -} - -// GATT timeout -internal struct Timeout { - - let start: Date - let timeout: TimeInterval - - var end: Date { + func write ( + _ characteristic: T, + for cache: [Characteristic], + response: Bool + ) async throws { + + guard let foundCharacteristic = cache.first(where: { $0.uuid == T.uuid }) + else { throw CentralError.invalidAttribute(T.uuid) } - return start + timeout + try await self.writeValue( + characteristic.data, + for: foundCharacteristic, + withResponse: response + ) } - init(start: Date = Date(), - timeout: TimeInterval) { + func read( + _ characteristic: T.Type, + for cache: [Characteristic] + ) async throws -> T { + + guard let foundCharacteristic = cache.first(where: { $0.uuid == T.uuid }) + else { throw CentralError.invalidAttribute(T.uuid) } + + let data = try await self.readValue(for: foundCharacteristic) + + guard let value = T.init(data: data) + else { throw GATTError.invalidData(data) } - self.start = start - self.timeout = timeout + return value } - @discardableResult - func timeRemaining(for date: Date = Date()) throws -> TimeInterval { + func notify( + _ characteristic: T.Type, + for cache: [Characteristic] + ) async throws -> AsyncIndefiniteStream { - let remaining = end.timeIntervalSince(date) + guard let foundCharacteristic = cache.first(where: { $0.uuid == T.uuid }) + else { throw CentralError.invalidAttribute(T.uuid) } - if remaining > 0 { - - return remaining - - } else { - - throw CentralError.timeout + let stream = try await self.notify(for: foundCharacteristic) + + return AsyncIndefiniteStream { continuation in + for try await data in stream { + guard let value = T.init(data: data) else { + throw GATTError.invalidData(data) + } + continuation(value) + } } } } -*/ From 4375a996f0c513b6c41e894eb51236445142e582 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 16 Sep 2022 23:38:56 -0700 Subject: [PATCH 014/229] Updated `LockGATTError` --- Sources/CoreLock/Bluetooth/GATTError.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Sources/CoreLock/Bluetooth/GATTError.swift b/Sources/CoreLock/Bluetooth/GATTError.swift index 5acb3d76..c8d0d11b 100644 --- a/Sources/CoreLock/Bluetooth/GATTError.swift +++ b/Sources/CoreLock/Bluetooth/GATTError.swift @@ -11,11 +11,20 @@ import Bluetooth /// Smart Lock GATT Error public enum LockGATTError: Error { + /// No service with UUID found. + case serviceNotFound(BluetoothUUID) + + /// No characteristic with UUID found. + case characteristicNotFound(BluetoothUUID) + + /// The characteristic's value could not be parsed. Invalid data. + case invalidCharacteristicValue(BluetoothUUID) + + /// Not a compatible peripheral + case incompatiblePeripheral(Error?) + /// Invalid data. case invalidData(Data?) - - /// Could not complete GATT operation. - case couldNotComplete } internal typealias GATTError = LockGATTError From 48fa6495942313e97aec7e03d56898ddd0113381 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 16 Sep 2022 23:40:21 -0700 Subject: [PATCH 015/229] Added `readInformation()` --- .../LockInformationCharacteristic.swift | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Sources/CoreLock/Bluetooth/LockInformationCharacteristic.swift b/Sources/CoreLock/Bluetooth/LockInformationCharacteristic.swift index d52be6e4..d7d847d9 100644 --- a/Sources/CoreLock/Bluetooth/LockInformationCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/LockInformationCharacteristic.swift @@ -45,3 +45,30 @@ public struct LockInformationCharacteristic: TLVCharacteristic, Equatable, Codab self.unlockActions = unlockActions } } + +// MARK: - Central + +public extension CentralManager { + + /// Read the lock's information characteristic. + func readInformation(for peripheral: Peripheral) async throws -> LockInformation { + try await connection(for: peripheral) { + try await $0.readInformation() + } + } +} + +public extension GATTConnection { + + /// Read the lock's information characteristic. + func readInformation() async throws -> LockInformation { + let characteristic = try await read(LockInformationCharacteristic.self) + return LockInformation( + id: characteristic.id, + buildVersion: characteristic.buildVersion, + version: characteristic.version, + status: characteristic.status, + unlockActions: Set(characteristic.unlockActions.map { $0 }) + ) + } +} From 2bc34821c676636c4058cd2b85c8671b8e3191d6 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 16 Sep 2022 23:56:22 -0700 Subject: [PATCH 016/229] Added `KeyCredentials` --- Sources/CoreLock/KeyCredentials.swift | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 Sources/CoreLock/KeyCredentials.swift diff --git a/Sources/CoreLock/KeyCredentials.swift b/Sources/CoreLock/KeyCredentials.swift new file mode 100644 index 00000000..93b04877 --- /dev/null +++ b/Sources/CoreLock/KeyCredentials.swift @@ -0,0 +1,20 @@ +// +// KeyCredentials.swift +// +// +// Created by Alsey Coleman Miller on 9/16/22. +// + +import Foundation + +public struct KeyCredentials: Equatable { + + public let id: UUID + + public let secret: KeyData + + public init(id: UUID, secret: KeyData) { + self.id = id + self.secret = secret + } +} From 2e10143d2bdda0a02fc39103d7fa69f4dab32bb6 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 16 Sep 2022 23:57:12 -0700 Subject: [PATCH 017/229] Added `unlock()` --- .../Bluetooth/UnlockCharacteristic.swift | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Sources/CoreLock/Bluetooth/UnlockCharacteristic.swift b/Sources/CoreLock/Bluetooth/UnlockCharacteristic.swift index 94a07dc0..ad77a159 100644 --- a/Sources/CoreLock/Bluetooth/UnlockCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/UnlockCharacteristic.swift @@ -50,3 +50,35 @@ public struct UnlockRequest: Equatable, Codable { self.action = action } } + +// MARK: - Central + +public extension CentralManager { + + /// Unlock action. + func unlock( + _ action: UnlockAction = .default, + using key: KeyCredentials, + for peripheral: Peripheral + ) async throws { + try await connection(for: peripheral) { + try await $0.unlock(action, using: key) + } + } +} + +public extension GATTConnection { + + /// Unlock action. + func unlock( + _ action: UnlockAction = .default, + using key: KeyCredentials + ) async throws { + let characteristicValue = try UnlockCharacteristic( + request: UnlockRequest(action: action), + using: key.secret, + id: key.id + ) + try await write(characteristicValue, response: true) + } +} From e92afa2e885328eefb78f9bb5c432d8b032fd438 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 01:33:46 -0700 Subject: [PATCH 018/229] Added `setup()` --- .../Bluetooth/SetupCharacteristic.swift | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Sources/CoreLock/Bluetooth/SetupCharacteristic.swift b/Sources/CoreLock/Bluetooth/SetupCharacteristic.swift index eba398b8..8ab49db3 100644 --- a/Sources/CoreLock/Bluetooth/SetupCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/SetupCharacteristic.swift @@ -70,3 +70,44 @@ public extension Key { ) } } + +// MARK: - Central + +public extension CentralManager { + + /// Setup lock. + @discardableResult + func setup( + _ request: SetupRequest, + using sharedSecret: KeyData, + for peripheral: Peripheral + ) async throws -> LockInformation { + try await connection(for: peripheral) { + // write setup request + try await $0.setup(request, using: sharedSecret) + // validate status + let information = try await $0.readInformation() + guard information.status != .setup else { + throw GATTError.invalidData(nil) + } + return information + } + } +} + +public extension GATTConnection { + + /// Setup lock. + func setup( + _ request: SetupRequest, + using sharedSecret: KeyData + ) async throws { + // encrypt owner key data + let characteristicValue = try SetupCharacteristic( + request: request, + sharedSecret: sharedSecret + ) + // write setup characteristic + try await write(characteristicValue) + } +} From 1f5a4f1ab7809135874ecc5ec2469ede79274100 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 01:49:54 -0700 Subject: [PATCH 019/229] Added `createKey()` --- .../CreateNewKeyCharacteristic.swift | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Sources/CoreLock/Bluetooth/CreateNewKeyCharacteristic.swift b/Sources/CoreLock/Bluetooth/CreateNewKeyCharacteristic.swift index 514039fd..c02dde43 100644 --- a/Sources/CoreLock/Bluetooth/CreateNewKeyCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/CreateNewKeyCharacteristic.swift @@ -83,3 +83,35 @@ public extension NewKey { self.created = created } } + +// MARK: - Central + +public extension CentralManager { + + /// Create new key. + func createKey( + _ newKey: CreateNewKeyRequest, + using key: KeyCredentials, + for peripheral: Peripheral + ) async throws { + try await connection(for: peripheral) { + try await $0.createKey(newKey, using: key) + } + } +} + +public extension GATTConnection { + + /// Create new key. + func createKey( + _ newKey: CreateNewKeyRequest, + using key: KeyCredentials + ) async throws { + let characteristicValue = try CreateNewKeyCharacteristic( + request: newKey, + using: key.secret, + id: key.id + ) + try await write(characteristicValue, response: true) + } +} From b1a2e03e1c17c98578e818f87706761ae2df2a72 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 01:51:30 -0700 Subject: [PATCH 020/229] Added `confirmKey()` --- .../ConfirmNewKeyCharacteristic.swift | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/Sources/CoreLock/Bluetooth/ConfirmNewKeyCharacteristic.swift b/Sources/CoreLock/Bluetooth/ConfirmNewKeyCharacteristic.swift index 6745bc76..9224db5b 100644 --- a/Sources/CoreLock/Bluetooth/ConfirmNewKeyCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/ConfirmNewKeyCharacteristic.swift @@ -25,13 +25,13 @@ public struct ConfirmNewKeyCharacteristic: TLVEncryptedCharacteristic, Codable, self.encryptedData = encryptedData } - public init(request: ConfirmNewKeyRequest, for key: UUID, sharedSecret: KeyData) throws { + public init(request: ConfirmNewKeyRequest, using sharedSecret: KeyData, id: UUID) throws { let requestData = try type(of: self).encoder.encode(request) - self.encryptedData = try EncryptedData(encrypt: requestData, using: sharedSecret, id: .zero) + self.encryptedData = try EncryptedData(encrypt: requestData, using: sharedSecret, id: id) } - public func decrypt(with sharedSecret: KeyData) throws -> ConfirmNewKeyRequest { + public func decrypt(using sharedSecret: KeyData) throws -> ConfirmNewKeyRequest { let data = try encryptedData.decrypt(using: sharedSecret) guard let value = try? type(of: self).decoder.decode(ConfirmNewKeyRequest.self, from: data) @@ -51,3 +51,35 @@ public struct ConfirmNewKeyRequest: Equatable, Codable { self.secret = secret } } + +// MARK: - Central + +public extension CentralManager { + + /// Confirm new key. + func confirmKey( + _ confirmation: ConfirmNewKeyRequest, + using key: KeyCredentials, + for peripheral: Peripheral + ) async throws { + try await connection(for: peripheral) { + try await $0.confirmKey(confirmation, using: key) + } + } +} + +public extension GATTConnection { + + /// Confirm new key. + func confirmKey( + _ confirmation: ConfirmNewKeyRequest, + using key: KeyCredentials + ) async throws { + let characteristicValue = try ConfirmNewKeyCharacteristic( + request: confirmation, + using: key.secret, + id: key.id + ) + try await write(characteristicValue, response: true) + } +} From c19586071d2c730a7a456b2787a2d6ab21da0833 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 02:00:27 -0700 Subject: [PATCH 021/229] Added `removeKey()` --- .../Bluetooth/RemoveKeyCharacteristic.swift | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Sources/CoreLock/Bluetooth/RemoveKeyCharacteristic.swift b/Sources/CoreLock/Bluetooth/RemoveKeyCharacteristic.swift index 73596e40..3e96ccb5 100644 --- a/Sources/CoreLock/Bluetooth/RemoveKeyCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/RemoveKeyCharacteristic.swift @@ -56,3 +56,37 @@ public struct RemoveKeyRequest: Equatable, Codable { self.type = type } } + +// MARK: - Central + +public extension CentralManager { + + /// Remove the specified key. + func removeKey( + _ id: UUID, + type: KeyType = .key, + using key: KeyCredentials, + for peripheral: Peripheral + ) async throws { + try await connection(for: peripheral) { + try await $0.removeKey(id, type: type, using: key) + } + } +} + +public extension GATTConnection { + + /// Remove the specified key. + func removeKey( + _ id: UUID, + type: KeyType = .key, + using key: KeyCredentials + ) async throws { + let characteristicValue = try RemoveKeyCharacteristic( + request: RemoveKeyRequest(id: id, type: type), + using: key.secret, + id: key.id + ) + try await write(characteristicValue, response: true) + } +} From f5fdae3cbe048ab57ed48636d123005057dd7962 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 11:22:30 -0700 Subject: [PATCH 022/229] Added `list()` --- Sources/CoreLock/Bluetooth/Central.swift | 8 ++-- .../Bluetooth/ListEventsCharacteristic.swift | 21 +++++++++ .../Bluetooth/ListKeysCharacteristic.swift | 45 +++++++++++++++++++ Sources/CoreLock/Bluetooth/Notification.swift | 42 +++++++++++++++++ 4 files changed, 112 insertions(+), 4 deletions(-) diff --git a/Sources/CoreLock/Bluetooth/Central.swift b/Sources/CoreLock/Bluetooth/Central.swift index 610d5807..2bc68c05 100644 --- a/Sources/CoreLock/Bluetooth/Central.swift +++ b/Sources/CoreLock/Bluetooth/Central.swift @@ -13,8 +13,8 @@ public extension CentralManager { func connection( for peripheral: Peripheral, - _ connection: (GATTConnection - ) async throws -> (T)) async throws -> T { + _ connection: (GATTConnection) async throws -> (T) + ) async throws -> T { // connect first try await self.connect(to: peripheral) @@ -47,8 +47,8 @@ public extension CentralManager { public struct GATTConnection { - internal let central: Central - + internal unowned let central: Central + public let maximumTransmissionUnit: GATT.MaximumTransmissionUnit internal let characteristics: [BluetoothUUID: [Characteristic]] diff --git a/Sources/CoreLock/Bluetooth/ListEventsCharacteristic.swift b/Sources/CoreLock/Bluetooth/ListEventsCharacteristic.swift index 90d717c9..45b0dd0e 100644 --- a/Sources/CoreLock/Bluetooth/ListEventsCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/ListEventsCharacteristic.swift @@ -48,3 +48,24 @@ public struct ListEventsRequest: Codable, Equatable { self.fetchRequest = fetchRequest } } + +// MARK: - Central + +public extension GATTConnection { + + /// Retreive a list of events on device. + func listEvents( + fetchRequest: LockEvent.FetchRequest? = nil, + using key: KeyCredentials, + log: ((String) -> ())? = nil + ) async throws -> AsyncThrowingStream { + let write = { + try ListEventsCharacteristic( + request: ListEventsRequest(fetchRequest: fetchRequest), + using: key.secret, + id: key.id + ) + } + return try await list(write(), EventsCharacteristic.self, key: key, log: log) + } +} diff --git a/Sources/CoreLock/Bluetooth/ListKeysCharacteristic.swift b/Sources/CoreLock/Bluetooth/ListKeysCharacteristic.swift index 08f1323a..e5c7cc86 100644 --- a/Sources/CoreLock/Bluetooth/ListKeysCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/ListKeysCharacteristic.swift @@ -25,3 +25,48 @@ public struct ListKeysCharacteristic: TLVCharacteristic, Codable, Equatable { self.authentication = authentication } } + +// MARK: - Central +/* +public extension CentralManager { + + /// Retreive a list of all keys on device. + func listKeys( + using key: KeyCredentials, + for peripheral: Peripheral, + notification: ((KeyListNotification) -> ())? = nil, + log: ((String) -> ())? = nil + ) async throws { + try await connect(to: peripheral) { + let stream = try await $0.listKeys(using: key, log: log) + //var list = KeysList() + for try await value in stream { + notification?(value) + //list.append(value.key) + } + //return list + } + } +} +*/ +public extension GATTConnection { + + /// Retreive a list of all keys on device. + func listKeys( + using key: KeyCredentials, + log: ((String) -> ())? = nil + ) async throws -> AsyncThrowingStream { + let write = { + ListKeysCharacteristic( + authentication: Authentication( + key: key.secret, + message: AuthenticationMessage( + digest: Digest(hash: Data()), + id: key.id + ) + ) + ) + } + return try await list(write(), KeysCharacteristic.self, key: key, log: log) + } +} diff --git a/Sources/CoreLock/Bluetooth/Notification.swift b/Sources/CoreLock/Bluetooth/Notification.swift index c77ed114..08631e50 100644 --- a/Sources/CoreLock/Bluetooth/Notification.swift +++ b/Sources/CoreLock/Bluetooth/Notification.swift @@ -44,3 +44,45 @@ public extension GATTEncryptedNotification { return chunk.data } } + +internal extension GATTConnection { + + func list( + _ write: @autoclosure () throws -> (Write), + _ notify: ChunkNotification.Type, + key: KeyCredentials, + log: ((String) -> ())? = nil + ) async throws -> AsyncThrowingStream where Write: GATTProfileCharacteristic, ChunkNotification: GATTEncryptedNotification { + let stream = try await self.notify(ChunkNotification.self) + let writeValue = try write() + try await self.write(writeValue) + return AsyncThrowingStream(ChunkNotification.Notification.self, bufferingPolicy: .unbounded) { continuation in + Task.detached { + do { + var chunks = [Chunk]() + chunks.reserveCapacity(2) + for try await chunkNotification in stream { + let chunk = chunkNotification.chunk + log?("Received chunk \(chunks.count + 1) (\(chunk.bytes.count) bytes)") + chunks.append(chunk) + assert(chunks.isEmpty == false) + guard chunks.length >= chunk.total else { + continue // wait for more chunks + } + let notificationValue = try ChunkNotification.from(chunks: chunks, using: key.secret) + chunks.removeAll(keepingCapacity: true) + continuation.yield(notificationValue) + guard notificationValue.isLast else { + continue // wait for final value + } + stream.stop() + } + continuation.finish() + } catch { + stream.stop() + continuation.finish(throwing: error) + } + } + } + } +} From d34aec4176d5eff3087e54ba948a92c43ee6efd1 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 11:22:50 -0700 Subject: [PATCH 023/229] Updated `GATTServiceController` for Swift 5.7 --- Sources/CoreLockGATTServer/GATTServiceController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CoreLockGATTServer/GATTServiceController.swift b/Sources/CoreLockGATTServer/GATTServiceController.swift index 561dc636..65224b9d 100644 --- a/Sources/CoreLockGATTServer/GATTServiceController.swift +++ b/Sources/CoreLockGATTServer/GATTServiceController.swift @@ -9,7 +9,7 @@ import Foundation import Bluetooth import GATT -public protocol GATTServiceController: class { +public protocol GATTServiceController: AnyObject { associatedtype Peripheral: PeripheralProtocol From f918082d6a79ba315147b05469d2c475c0a9d7b5 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 12:58:06 -0700 Subject: [PATCH 024/229] Added `TLVCodable` conformance for `EncryptedData` --- Sources/CoreLock/Bluetooth/TLV.swift | 15 +++++++++++-- Sources/CoreLock/Crypto/EncryptedData.swift | 24 +++++++++++++++++++++ Tests/CoreLockTests/GATTProfileTests.swift | 9 +------- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/Sources/CoreLock/Bluetooth/TLV.swift b/Sources/CoreLock/Bluetooth/TLV.swift index 9abae795..ea35e259 100644 --- a/Sources/CoreLock/Bluetooth/TLV.swift +++ b/Sources/CoreLock/Bluetooth/TLV.swift @@ -62,14 +62,14 @@ public extension TLVCharacteristic where Self: Codable { } } -public protocol TLVEncryptedCharacteristic: TLVCharacteristic { +public protocol TLVEncryptedCharacteristic: TLVCharacteristic, TLVCodable { var encryptedData: EncryptedData { get } init(encryptedData: EncryptedData) } -public extension TLVEncryptedCharacteristic where Self: Codable { +public extension TLVEncryptedCharacteristic { init(from decoder: Decoder) throws { let encryptedData = try EncryptedData(from: decoder) @@ -79,4 +79,15 @@ public extension TLVEncryptedCharacteristic where Self: Codable { func encode(to encoder: Encoder) throws { try self.encryptedData.encode(to: encoder) } + + init?(tlvData: Data) { + guard let encryptedData = EncryptedData(tlvData: tlvData) else { + return nil + } + self.init(encryptedData: encryptedData) + } + + var tlvData: Data { + encryptedData.tlvData + } } diff --git a/Sources/CoreLock/Crypto/EncryptedData.swift b/Sources/CoreLock/Crypto/EncryptedData.swift index f308ac69..d77deca4 100644 --- a/Sources/CoreLock/Crypto/EncryptedData.swift +++ b/Sources/CoreLock/Crypto/EncryptedData.swift @@ -6,6 +6,7 @@ // import Foundation +import TLVCoding public struct EncryptedData: Equatable, Codable { @@ -35,3 +36,26 @@ public extension EncryptedData { return try CoreLock.decrypt(encryptedData, using: key, authentication: authentication.message) } } + +extension EncryptedData: TLVCodable { + + internal static var authenticationPrefixLength: Int { 176 } + + public init?(tlvData: Data) { + let prefixLength = Self.authenticationPrefixLength + guard tlvData.count >= prefixLength else { + return nil + } + let prefix = Data(tlvData.prefix(prefixLength)) + guard let authentication = try? TLVDecoder.lock.decode(Authentication.self, from: prefix) else { + return nil + } + self.authentication = authentication + self.encryptedData = tlvData.count > prefixLength ? Data(tlvData.suffix(from: prefixLength)) : Data() + } + + public var tlvData: Data { + let authenticationData = try! TLVEncoder.lock.encode(authentication) + return authenticationData + encryptedData + } +} diff --git a/Tests/CoreLockTests/GATTProfileTests.swift b/Tests/CoreLockTests/GATTProfileTests.swift index 23fef2d1..5848b419 100644 --- a/Tests/CoreLockTests/GATTProfileTests.swift +++ b/Tests/CoreLockTests/GATTProfileTests.swift @@ -38,18 +38,11 @@ final class GATTProfileTests: XCTestCase { let decodedRequest = try decodedCharacteristic.decrypt(with: key.secret) XCTAssertEqual(decodedRequest, request) XCTAssertEqual(characteristic, decodedCharacteristic) - XCTAssertEqual(characteristic.encryptedData.encryptedData, decodedCharacteristic.encryptedData.encryptedData) - XCTAssertEqual(characteristic.encryptedData.authentication, decodedCharacteristic.encryptedData.authentication) - XCTAssertEqual(characteristic.encryptedData.authentication.message, decodedCharacteristic.encryptedData.authentication.message) - XCTAssertEqual(characteristic.encryptedData.authentication.message.nonce, decodedCharacteristic.encryptedData.authentication.message.nonce) - XCTAssertEqual(characteristic.encryptedData.authentication.message.date, decodedCharacteristic.encryptedData.authentication.message.date) - XCTAssertEqual(characteristic.encryptedData.authentication.message.id, decodedCharacteristic.encryptedData.authentication.message.id) - XCTAssertEqual(characteristic.encryptedData.authentication.signedData, decodedCharacteristic.encryptedData.authentication.signedData) XCTAssert(decodedCharacteristic.encryptedData.authentication.isAuthenticated(using: key.secret)) XCTAssert(characteristic.encryptedData.authentication.isAuthenticated(using: key.secret)) XCTAssert(Authentication(key: key.secret, message: characteristic.encryptedData.authentication.message).isAuthenticated(using: key.secret)) - try await Task.sleep(nanoseconds: 100_000_000) + try await Task.sleep(nanoseconds: 10_000_000) } } From 8a3653dc33392b216c7217e2c88e895526c1c889 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 12:58:22 -0700 Subject: [PATCH 025/229] Updated dependencies --- Package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.resolved b/Package.resolved index 5ba754f7..67f428b2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -60,7 +60,7 @@ "location" : "https://github.com/PureSwift/TLVCoding.git", "state" : { "branch" : "master", - "revision" : "548e984056146722f148794a48f18f093c778097" + "revision" : "95e608efc42369e785ac73bdb60fbf69dfae81b8" } } ], From 6f1073f8b6c678fdedda1afadf0c309fc6475c87 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 14:37:51 -0700 Subject: [PATCH 026/229] Updated `CoreLockGATTServer` for Swift 5.7 --- Package.swift | 3 +- .../CreateNewKeyCharacteristic.swift | 2 +- .../Bluetooth/ListEventsCharacteristic.swift | 2 +- .../Bluetooth/UnlockCharacteristic.swift | 2 +- .../CoreLock/Networking/EventsResponse.swift | 2 +- .../CoreLock/Networking/KeysResponse.swift | 2 +- .../CoreLock/Networking/LockNetService.swift | 2 +- .../DeviceInformationService.swift | 87 ++++--- .../GATTServiceController.swift | 17 +- .../LockGATTController.swift | 56 ++--- .../LockServiceController.swift | 233 +++++++++--------- 11 files changed, 207 insertions(+), 201 deletions(-) diff --git a/Package.swift b/Package.swift index 2c511930..d148760f 100644 --- a/Package.swift +++ b/Package.swift @@ -60,11 +60,10 @@ let package = Package( .product(name: "Bluetooth", package: "Bluetooth"), ] ), - /* .target( name: "CoreLockGATTServer", dependencies: ["CoreLock"] - ),*/ + ), .testTarget( name: "CoreLockTests", dependencies: ["CoreLock"] diff --git a/Sources/CoreLock/Bluetooth/CreateNewKeyCharacteristic.swift b/Sources/CoreLock/Bluetooth/CreateNewKeyCharacteristic.swift index c02dde43..99717342 100644 --- a/Sources/CoreLock/Bluetooth/CreateNewKeyCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/CreateNewKeyCharacteristic.swift @@ -31,7 +31,7 @@ public struct CreateNewKeyCharacteristic: TLVEncryptedCharacteristic, Codable, E self.encryptedData = try EncryptedData(encrypt: requestData, using: key, id: id) } - public func decrypt(with sharedSecret: KeyData) throws -> CreateNewKeyRequest { + public func decrypt(using sharedSecret: KeyData) throws -> CreateNewKeyRequest { let data = try encryptedData.decrypt(using: sharedSecret) guard let value = try? type(of: self).decoder.decode(CreateNewKeyRequest.self, from: data) diff --git a/Sources/CoreLock/Bluetooth/ListEventsCharacteristic.swift b/Sources/CoreLock/Bluetooth/ListEventsCharacteristic.swift index 45b0dd0e..1e1e034b 100644 --- a/Sources/CoreLock/Bluetooth/ListEventsCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/ListEventsCharacteristic.swift @@ -30,7 +30,7 @@ public struct ListEventsCharacteristic: TLVEncryptedCharacteristic, Codable, Equ self.encryptedData = try EncryptedData(encrypt: requestData, using: key, id: id) } - public func decrypt(with key: KeyData) throws -> ListEventsRequest { + public func decrypt(using key: KeyData) throws -> ListEventsRequest { let data = try encryptedData.decrypt(using: key) guard let value = try? type(of: self).decoder.decode(ListEventsRequest.self, from: data) diff --git a/Sources/CoreLock/Bluetooth/UnlockCharacteristic.swift b/Sources/CoreLock/Bluetooth/UnlockCharacteristic.swift index ad77a159..eed92431 100644 --- a/Sources/CoreLock/Bluetooth/UnlockCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/UnlockCharacteristic.swift @@ -30,7 +30,7 @@ public struct UnlockCharacteristic: TLVEncryptedCharacteristic, Codable, Equatab self.encryptedData = try EncryptedData(encrypt: requestData, using: key, id: id) } - public func decrypt(with sharedSecret: KeyData) throws -> UnlockRequest { + public func decrypt(using sharedSecret: KeyData) throws -> UnlockRequest { let data = try encryptedData.decrypt(using: sharedSecret) guard let value = try? type(of: self).decoder.decode(UnlockRequest.self, from: data) diff --git a/Sources/CoreLock/Networking/EventsResponse.swift b/Sources/CoreLock/Networking/EventsResponse.swift index baddf206..4e7fe9e6 100644 --- a/Sources/CoreLock/Networking/EventsResponse.swift +++ b/Sources/CoreLock/Networking/EventsResponse.swift @@ -38,7 +38,7 @@ public extension EventsResponse { self.encryptedData = try .init(encrypt: data, with: key) } - func decrypt(with key: KeyData, + func decrypt(using key: KeyData, decoder: JSONDecoder = JSONDecoder()) throws -> EventsList { let data = try encryptedData.decrypt(using: key) diff --git a/Sources/CoreLock/Networking/KeysResponse.swift b/Sources/CoreLock/Networking/KeysResponse.swift index a7faabf0..05e8bc38 100644 --- a/Sources/CoreLock/Networking/KeysResponse.swift +++ b/Sources/CoreLock/Networking/KeysResponse.swift @@ -38,7 +38,7 @@ public extension KeysResponse { self.encryptedData = try .init(encrypt: data, with: key) } - func decrypt(with key: KeyData, + func decrypt(using key: KeyData, decoder: JSONDecoder = JSONDecoder()) throws -> KeysList { let data = try encryptedData.decrypt(using: key) diff --git a/Sources/CoreLock/Networking/LockNetService.swift b/Sources/CoreLock/Networking/LockNetService.swift index e0b4bdbd..7646eaca 100644 --- a/Sources/CoreLock/Networking/LockNetService.swift +++ b/Sources/CoreLock/Networking/LockNetService.swift @@ -214,7 +214,7 @@ extension LockNetService.EncryptedData { catch { throw AuthenticationError.encryptionError(error) } } - func decrypt(with key: KeyData) throws -> Data { + func decrypt(using key: KeyData) throws -> Data { // attempt to decrypt do { return try CoreLock.decrypt(key: key.data, iv: initializationVector, data: encryptedData) } diff --git a/Sources/CoreLockGATTServer/DeviceInformationService.swift b/Sources/CoreLockGATTServer/DeviceInformationService.swift index 1c16f4b9..bfb4bf7a 100644 --- a/Sources/CoreLockGATTServer/DeviceInformationService.swift +++ b/Sources/CoreLockGATTServer/DeviceInformationService.swift @@ -10,7 +10,7 @@ import Bluetooth import GATT import CoreLock -public final class GATTDeviceInformationServiceController : GATTServiceController { +public final class GATTDeviceInformationServiceController : GATTServiceController { public static var service: BluetoothUUID { return .deviceInformation } @@ -20,9 +20,7 @@ public final class GATTDeviceInformationServiceController Bool { + let characteristics: [GATTCharacteristic.Type] = [ + GATTManufacturerNameString.self, + GATTFirmwareRevisionString.self, + GATTSoftwareRevisionString.self, + GATTSerialNumberString.self, + GATTModelNumber.self, + GATTHardwareRevisionString.self, + ] + return characteristics.contains(where: { $0.uuid == characteristicUUID }) } - // MARK: - Methods + public func setHardware(_ newValue: LockHardware) async { + self.hardware = newValue + await updateInformation() + } - private func updateInformation() { + private func updateInformation() async { - self.modelNumber = GATTModelNumber(rawValue: hardware.model.rawValue) - self.serialNumber = GATTSerialNumberString(rawValue: hardware.serialNumber) - self.hardwareRevision = GATTHardwareRevisionString(rawValue: hardware.hardwareRevision) + modelNumber = GATTModelNumber(rawValue: hardware.model.rawValue) + await peripheral.write(modelNumber.data, forCharacteristic: modelNumberHandle) + serialNumber = GATTSerialNumberString(rawValue: hardware.serialNumber) + await peripheral.write(serialNumber.data, forCharacteristic: serialNumberHandle) + hardwareRevision = GATTHardwareRevisionString(rawValue: hardware.hardwareRevision) + await peripheral.write(hardwareRevision.data, forCharacteristic: hardwareRevisionHandle) } } diff --git a/Sources/CoreLockGATTServer/GATTServiceController.swift b/Sources/CoreLockGATTServer/GATTServiceController.swift index 65224b9d..b2216d14 100644 --- a/Sources/CoreLockGATTServer/GATTServiceController.swift +++ b/Sources/CoreLockGATTServer/GATTServiceController.swift @@ -8,33 +8,32 @@ import Foundation import Bluetooth import GATT +import CoreLock public protocol GATTServiceController: AnyObject { - associatedtype Peripheral: PeripheralProtocol + associatedtype Peripheral: PeripheralManager static var service: BluetoothUUID { get } - var characteristics: Set { get } - var peripheral: Peripheral { get } - init(peripheral: Peripheral) throws + init(peripheral: Peripheral) async throws - func willRead(_ request: GATTReadRequest) -> ATT.Error? + func willRead(_ request: GATTReadRequest) -> ATTError? - func willWrite(_ request: GATTWriteRequest) -> ATT.Error? + func willWrite(_ request: GATTWriteRequest) -> ATTError? - func didWrite(_ request: GATTWriteConfirmation) + func didWrite(_ request: GATTWriteConfirmation) async } public extension GATTServiceController { - func willRead(_ request: GATTReadRequest) -> ATT.Error? { + func willRead(_ request: GATTReadRequest) -> ATTError? { return nil } - func willWrite(_ request: GATTWriteRequest) -> ATT.Error? { + func willWrite(_ request: GATTWriteRequest) -> ATTError? { return nil } diff --git a/Sources/CoreLockGATTServer/LockGATTController.swift b/Sources/CoreLockGATTServer/LockGATTController.swift index eda2794e..5476cc24 100644 --- a/Sources/CoreLockGATTServer/LockGATTController.swift +++ b/Sources/CoreLockGATTServer/LockGATTController.swift @@ -5,19 +5,13 @@ // Created by Alsey Coleman Miller on 8/11/18. // -#if os(Linux) -import Glibc -#elseif os(macOS) -import Darwin -#endif - import Foundation import Bluetooth import GATT import CoreLock /// Smart Lock GATT Server controller. -public final class LockGATTController { +public final class LockGATTController { // MARK: - Properties @@ -28,32 +22,41 @@ public final class LockGATTController { public let lockServiceController: LockGATTServiceController // Lock hardware model - public var hardware: LockHardware = .empty { - didSet { - self.lockServiceController.hardware = hardware - self.deviceInformationController.hardware = hardware - } - } + public private(set) var hardware: LockHardware = .empty // MARK: - Initialization - public init(peripheral: Peripheral) throws { + public init(peripheral: Peripheral) async throws { self.peripheral = peripheral // load services - self.deviceInformationController = try GATTDeviceInformationServiceController(peripheral: peripheral) - self.lockServiceController = try LockGATTServiceController(peripheral: peripheral) + self.deviceInformationController = try await GATTDeviceInformationServiceController(peripheral: peripheral) + self.lockServiceController = try await LockGATTServiceController(peripheral: peripheral) // set callbacks - self.peripheral.willRead = { [unowned self] in self.willRead($0) } - self.peripheral.willWrite = { [unowned self] in return self.willWrite($0) } - self.peripheral.didWrite = { [unowned self] in self.didWrite($0) } + self.peripheral.willRead = { [unowned self] in + self.willRead($0) + } + self.peripheral.willWrite = { [unowned self] in + return self.willWrite($0) + } + self.peripheral.didWrite = { (confirmation) in + Task(priority: .high) { [weak self] in + await self?.didWrite(confirmation) + } + } } // MARK: - Methods - private func willRead(_ request: GATTReadRequest) -> ATT.Error? { + public func setHardware(_ newValue: LockHardware) async { + self.hardware = newValue + await deviceInformationController.setHardware(newValue) + await deviceInformationController.setHardware(newValue) + } + + private func willRead(_ request: GATTReadRequest) -> ATTError? { if lockServiceController.supportsCharacteristic(request.uuid) { return lockServiceController.willRead(request) @@ -64,7 +67,7 @@ public final class LockGATTController { } } - private func willWrite(_ request: GATTWriteRequest) -> ATT.Error? { + private func willWrite(_ request: GATTWriteRequest) -> ATTError? { if lockServiceController.supportsCharacteristic(request.uuid) { return lockServiceController.willWrite(request) @@ -75,19 +78,12 @@ public final class LockGATTController { } } - private func didWrite(_ confirmation: GATTWriteConfirmation) { + private func didWrite(_ confirmation: GATTWriteConfirmation) async { if lockServiceController.supportsCharacteristic(confirmation.uuid) { - lockServiceController.didWrite(confirmation) + await lockServiceController.didWrite(confirmation) } else if deviceInformationController.supportsCharacteristic(confirmation.uuid) { deviceInformationController.didWrite(confirmation) } } } - -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 d8013e1a..6984f7d8 100644 --- a/Sources/CoreLockGATTServer/LockServiceController.swift +++ b/Sources/CoreLockGATTServer/LockServiceController.swift @@ -10,7 +10,7 @@ import Bluetooth import GATT import CoreLock -public final class LockGATTServiceController : GATTServiceController { +public final class LockGATTServiceController : GATTServiceController { public static var service: BluetoothUUID { return Service.uuid } @@ -22,17 +22,11 @@ public final class LockGATTServiceController : public let peripheral: Peripheral - public var hardware: LockHardware = .empty { - didSet { updateInformation() } - } + public private(set) var hardware: LockHardware = .empty - public var configurationStore: LockConfigurationStore = InMemoryLockConfigurationStore() { - didSet { updateInformation() } - } + public var configurationStore: LockConfigurationStore = InMemoryLockConfigurationStore() - public var authorization: LockAuthorizationStore = InMemoryLockAuthorization() { - didSet { updateInformation() } - } + public var authorization: LockAuthorizationStore = InMemoryLockAuthorization() public var setupSecret: KeyData = KeyData() @@ -59,60 +53,60 @@ public final class LockGATTServiceController : // MARK: - Initialization - public init(peripheral: Peripheral) throws { + public init(peripheral: Peripheral) async throws { self.peripheral = peripheral let characteristics = [ - GATT.Characteristic(uuid: LockInformationCharacteristic.uuid, + GATTAttribute.Characteristic(uuid: LockInformationCharacteristic.uuid, value: Data(), permissions: [.read], properties: LockInformationCharacteristic.properties), - GATT.Characteristic(uuid: SetupCharacteristic.uuid, + GATTAttribute.Characteristic(uuid: SetupCharacteristic.uuid, value: Data(), permissions: [.write], properties: SetupCharacteristic.properties), - GATT.Characteristic(uuid: UnlockCharacteristic.uuid, + GATTAttribute.Characteristic(uuid: UnlockCharacteristic.uuid, value: Data(), permissions: [.write], properties: UnlockCharacteristic.properties), - GATT.Characteristic(uuid: CreateNewKeyCharacteristic.uuid, + GATTAttribute.Characteristic(uuid: CreateNewKeyCharacteristic.uuid, value: Data(), permissions: [.write], properties: CreateNewKeyCharacteristic.properties), - GATT.Characteristic(uuid: ConfirmNewKeyCharacteristic.uuid, + GATTAttribute.Characteristic(uuid: ConfirmNewKeyCharacteristic.uuid, value: Data(), permissions: [.write], properties: ConfirmNewKeyCharacteristic.properties), - GATT.Characteristic(uuid: RemoveKeyCharacteristic.uuid, + GATTAttribute.Characteristic(uuid: RemoveKeyCharacteristic.uuid, value: Data(), permissions: [.write], properties: RemoveKeyCharacteristic.properties), - GATT.Characteristic(uuid: ListKeysCharacteristic.uuid, + GATTAttribute.Characteristic(uuid: ListKeysCharacteristic.uuid, value: Data(), permissions: [.write], properties: ListKeysCharacteristic.properties), - GATT.Characteristic(uuid: KeysCharacteristic.uuid, + GATTAttribute.Characteristic(uuid: KeysCharacteristic.uuid, value: Data(), permissions: [], properties: KeysCharacteristic.properties, descriptors: [GATTClientCharacteristicConfiguration().descriptor]), - GATT.Characteristic(uuid: ListEventsCharacteristic.uuid, + GATTAttribute.Characteristic(uuid: ListEventsCharacteristic.uuid, value: Data(), permissions: [.write], properties: ListEventsCharacteristic.properties), - GATT.Characteristic(uuid: EventsCharacteristic.uuid, + GATTAttribute.Characteristic(uuid: EventsCharacteristic.uuid, value: Data(), permissions: [], properties: EventsCharacteristic.properties, @@ -121,39 +115,44 @@ public final class LockGATTServiceController : self.characteristics = Set(characteristics.map { $0.uuid }) - let service = GATT.Service(uuid: Service.uuid, + let service = GATTAttribute.Service(uuid: Service.uuid, primary: Service.isPrimary, characteristics: characteristics) - self.serviceHandle = try peripheral.add(service: service) + self.serviceHandle = try await peripheral.add(service: service) - self.informationHandle = peripheral.characteristics(for: LockInformationCharacteristic.uuid)[0] - self.setupHandle = peripheral.characteristics(for: SetupCharacteristic.uuid)[0] - self.unlockHandle = peripheral.characteristics(for: UnlockCharacteristic.uuid)[0] - self.createNewKeyHandle = peripheral.characteristics(for: CreateNewKeyCharacteristic.uuid)[0] - self.confirmNewKeyHandle = peripheral.characteristics(for: ConfirmNewKeyCharacteristic.uuid)[0] - self.removeKeyHandle = peripheral.characteristics(for: RemoveKeyCharacteristic.uuid)[0] - self.keysRequestHandle = peripheral.characteristics(for: ListKeysCharacteristic.uuid)[0] - self.keysResponseHandle = peripheral.characteristics(for: KeysCharacteristic.uuid)[0] - self.eventsRequestHandle = peripheral.characteristics(for: ListEventsCharacteristic.uuid)[0] - self.eventsResponseHandle = peripheral.characteristics(for: EventsCharacteristic.uuid)[0] + self.informationHandle = await peripheral.characteristics(for: LockInformationCharacteristic.uuid)[0] + self.setupHandle = await peripheral.characteristics(for: SetupCharacteristic.uuid)[0] + self.unlockHandle = await peripheral.characteristics(for: UnlockCharacteristic.uuid)[0] + self.createNewKeyHandle = await peripheral.characteristics(for: CreateNewKeyCharacteristic.uuid)[0] + self.confirmNewKeyHandle = await peripheral.characteristics(for: ConfirmNewKeyCharacteristic.uuid)[0] + self.removeKeyHandle = await peripheral.characteristics(for: RemoveKeyCharacteristic.uuid)[0] + self.keysRequestHandle = await peripheral.characteristics(for: ListKeysCharacteristic.uuid)[0] + self.keysResponseHandle = await peripheral.characteristics(for: KeysCharacteristic.uuid)[0] + self.eventsRequestHandle = await peripheral.characteristics(for: ListEventsCharacteristic.uuid)[0] + self.eventsResponseHandle = await peripheral.characteristics(for: EventsCharacteristic.uuid)[0] - updateInformation() + await updateInformation() } - deinit { - self.peripheral.remove(service: serviceHandle) + // MARK: - Methods + + func supportsCharacteristic(_ characteristicUUID: BluetoothUUID) -> Bool { + return Self.Service.characteristics.contains(where: { $0.uuid == characteristicUUID }) } - // MARK: - Methods + public func setHardware(_ newValue: LockHardware) async { + self.hardware = newValue + await updateInformation() + } - public func reset() { + public func reset() async { try? authorization.removeAll() - updateInformation() + await updateInformation() } - public func willRead(_ request: GATTReadRequest) -> ATT.Error? { + public func willRead(_ request: GATTReadRequest) -> ATTError? { switch request.handle { case informationHandle: @@ -164,7 +163,7 @@ public final class LockGATTServiceController : } } - public func willWrite(_ request: GATTWriteRequest) -> ATT.Error? { + public func willWrite(_ request: GATTWriteRequest) -> ATTError? { switch request.handle { case setupHandle: @@ -178,7 +177,7 @@ public final class LockGATTServiceController : } } - public func didWrite(_ write: GATTWriteConfirmation) { + public func didWrite(_ write: GATTWriteConfirmation) async { switch write.handle { @@ -190,7 +189,7 @@ public final class LockGATTServiceController : guard let characteristic = SetupCharacteristic(data: write.value) else { print("Could not parse \(SetupCharacteristic.self)"); return } - setup(characteristic) + await setup(characteristic) case unlockHandle: @@ -200,7 +199,7 @@ public final class LockGATTServiceController : guard let characteristic = UnlockCharacteristic(data: write.value) else { print("Could not parse \(UnlockCharacteristic.self)"); return } - unlock(characteristic) + await unlock(characteristic) case createNewKeyHandle: @@ -240,7 +239,7 @@ public final class LockGATTServiceController : guard let characteristic = ListKeysCharacteristic(data: write.value) else { print("Could not parse \(ListKeysCharacteristic.self)"); return } - listKeysRequest(characteristic, maximumUpdateValueLength: write.maximumUpdateValueLength) + await listKeysRequest(characteristic, maximumUpdateValueLength: write.maximumUpdateValueLength) case keysResponseHandle: assertionFailure("Not writable") @@ -253,7 +252,7 @@ public final class LockGATTServiceController : guard let characteristic = ListEventsCharacteristic(data: write.value) else { print("Could not parse \(ListEventsCharacteristic.self)"); return } - listEventsRequest(characteristic, maximumUpdateValueLength: write.maximumUpdateValueLength) + await listEventsRequest(characteristic, maximumUpdateValueLength: write.maximumUpdateValueLength) case eventsResponseHandle: assertionFailure("Not writable") @@ -265,22 +264,19 @@ public final class LockGATTServiceController : // MARK: - Private Methods - private func updateInformation() { + private func updateInformation() async { let status: LockStatus = authorization.isEmpty ? .setup : .unlock - - let identifier = configurationStore.configuration.identifier - - let information = LockInformationCharacteristic(identifier: identifier, + let id = configurationStore.configuration.id + let information = LockInformationCharacteristic(id: id, buildVersion: .current, version: .current, status: status, unlockActions: [.default]) - - peripheral[characteristic: informationHandle] = information.data + await peripheral.write(information.data, forCharacteristic: informationHandle) } - private func setup(_ setup: SetupCharacteristic) { + private func setup(_ setup: SetupCharacteristic) async { assert(authorization.isEmpty) @@ -308,32 +304,32 @@ public final class LockGATTServiceController : print("Lock setup completed") - updateInformation() + await updateInformation() - try events.save(.setup(.init(key: ownerKey.identifier))) + try events.save(.setup(.init(key: ownerKey.id))) lockChanged?() } catch { print("Setup error: \(error)") } } - private func unlock(_ unlock: UnlockCharacteristic) { + private func unlock(_ characteristic: UnlockCharacteristic) async { - let keyIdentifier = unlock.identifier + let keyIdentifier = characteristic.encryptedData.authentication.message.id do { guard let (key, secret) = try authorization.key(for: keyIdentifier) else { print("Unknown key \(keyIdentifier)"); return } - assert(key.identifier == keyIdentifier, "Invalid key") + assert(key.id == keyIdentifier, "Invalid key") // validate HMAC - guard unlock.authentication.isAuthenticated(with: secret) + guard characteristic.encryptedData.authentication.isAuthenticated(using: secret) else { print("Invalid key secret"); return } // guard against replay attacks - let timestamp = unlock.authentication.message.date + let timestamp = characteristic.encryptedData.authentication.message.date let now = Date() guard timestamp <= now + authorizationTimeout, // cannot be used later for replay attacks timestamp > now - authorizationTimeout // only valid for 5 seconds @@ -345,12 +341,14 @@ public final class LockGATTServiceController : else { print("Cannot unlock during schedule"); return } } + let request = try characteristic.decrypt(using: secret) + // unlock with the specified action - try unlockDelegate.unlock(unlock.action) + try unlockDelegate.unlock(request.action) - print("Key \(key.identifier) \(key.name) unlocked with action \(unlock.action)") + print("Key \(key.id) \(key.name) unlocked with action \(request.action)") - try events.save(.unlock(.init(key: key.identifier, action: unlock.action))) + try events.save(.unlock(.init(key: key.id, action: request.action))) lockChanged?() @@ -359,17 +357,17 @@ public final class LockGATTServiceController : private func createNewKey(_ characteristic: CreateNewKeyCharacteristic) { - let keyIdentifier = characteristic.identifier + let keyIdentifier = characteristic.encryptedData.authentication.message.id do { guard let (key, secret) = try authorization.key(for: keyIdentifier) else { print("Unknown key \(keyIdentifier)"); return } - assert(key.identifier == keyIdentifier, "Invalid key") + assert(key.id == keyIdentifier, "Invalid key") // validate HMAC - guard characteristic.encryptedData.authentication.isAuthenticated(with: secret) + guard characteristic.encryptedData.authentication.isAuthenticated(using: secret) else { print("Invalid key secret"); return } // guard against replay attacks @@ -391,9 +389,9 @@ public final class LockGATTServiceController : try self.authorization.add(newKey, secret: request.secret) - print("Key \(keyIdentifier) \(key.name) created new key \(request.identifier)") + print("Key \(keyIdentifier) \(key.name) created new key \(request.id)") - try events.save(.createNewKey(.init(key: key.identifier, newKey: newKey.identifier))) + try events.save(.createNewKey(.init(key: key.id, newKey: newKey.id))) lockChanged?() @@ -402,17 +400,17 @@ public final class LockGATTServiceController : private func confirmNewKey(_ characteristic: ConfirmNewKeyCharacteristic) { - let newKeyIdentifier = characteristic.identifier + let newKeyIdentifier = characteristic.encryptedData.authentication.message.id do { guard let (newKey, secret) = try authorization.newKey(for: newKeyIdentifier) else { print("Unknown key \(newKeyIdentifier)"); return } - assert(newKey.identifier == newKeyIdentifier, "Invalid key") + assert(newKey.id == newKeyIdentifier, "Invalid key") // validate HMAC - guard characteristic.encryptedData.authentication.isAuthenticated(with: secret) + guard characteristic.encryptedData.authentication.isAuthenticated(using: secret) else { print("Invalid key secret"); return } // guard against replay attacks @@ -426,7 +424,7 @@ public final class LockGATTServiceController : let request = try characteristic.decrypt(using: secret) let keySecret = request.secret let key = Key( - identifier: newKey.identifier, + id: newKey.id, name: newKey.name, created: newKey.created, permission: newKey.permission @@ -437,9 +435,9 @@ public final class LockGATTServiceController : print("Key \(newKeyIdentifier) \(key.name) confirmed with shared secret") - assert(try! authorization.key(for: key.identifier) != nil, "Key not stored") + assert(try! authorization.key(for: key.id) != nil, "Key not stored") - try events.save(.confirmNewKey(.init(newKey: newKey.identifier, key: key.identifier))) + try events.save(.confirmNewKey(.init(newKey: newKey.id, key: key.id))) lockChanged?() @@ -448,21 +446,21 @@ public final class LockGATTServiceController : private func removeKey(_ characteristic: RemoveKeyCharacteristic) { - let keyIdentifier = characteristic.identifier + let keyIdentifier = characteristic.encryptedData.authentication.message.id do { guard let (key, secret) = try authorization.key(for: keyIdentifier) else { print("Unknown key \(keyIdentifier)"); return } - assert(key.identifier == keyIdentifier, "Invalid key") + assert(key.id == keyIdentifier, "Invalid key") // validate HMAC - guard characteristic.authentication.isAuthenticated(with: secret) + guard characteristic.encryptedData.authentication.isAuthenticated(using: secret) else { print("Invalid key secret"); return } // guard against replay attacks - let timestamp = characteristic.authentication.message.date + let timestamp = characteristic.encryptedData.authentication.message.date let now = Date() guard timestamp <= now + authorizationTimeout, // cannot be used later for replay attacks timestamp > now - authorizationTimeout // only valid for 5 seconds @@ -474,41 +472,44 @@ public final class LockGATTServiceController : return } - switch characteristic.type { + // decrypt + let request = try characteristic.decrypt(using: secret) + + switch request.type { case .key: - guard let (removeKey, _) = try authorization.key(for: characteristic.key) - else { print("Key \(characteristic.key) does not exist"); return } - assert(removeKey.identifier == characteristic.key) - try authorization.removeKey(removeKey.identifier) + guard let (removeKey, _) = try authorization.key(for: request.id) + else { print("Key \(request.id) does not exist"); return } + assert(removeKey.id == request.id) + try authorization.removeKey(removeKey.id) case .newKey: - guard let (removeKey, _) = try authorization.newKey(for: characteristic.key) - else { print("New Key \(characteristic.key) does not exist"); return } - assert(removeKey.identifier == characteristic.key) - try authorization.removeNewKey(removeKey.identifier) + guard let (removeKey, _) = try authorization.newKey(for: request.id) + else { print("New Key \(request.id) does not exist"); return } + assert(removeKey.id == request.id) + try authorization.removeNewKey(removeKey.id) } - print("Key \(key.identifier) \(key.name) removed \(characteristic.type) \(characteristic.key)") + print("Key \(key.id) \(key.name) removed \(request.type) \(request.id)") - try events.save(.removeKey(.init(key: key.identifier, removedKey: characteristic.key, type: characteristic.type))) + try events.save(.removeKey(.init(key: key.id, removedKey: request.id, type: request.type))) lockChanged?() } catch { print("Remove key error: \(error)") } } - private func listKeysRequest(_ characteristic: ListKeysCharacteristic, maximumUpdateValueLength: Int) { + private func listKeysRequest(_ characteristic: ListKeysCharacteristic, maximumUpdateValueLength: Int) async { - let keyIdentifier = characteristic.identifier + let keyIdentifier = characteristic.authentication.message.id do { guard let (key, secret) = try authorization.key(for: keyIdentifier) else { print("Unknown key \(keyIdentifier)"); return } - assert(key.identifier == keyIdentifier, "Invalid key") + assert(key.id == keyIdentifier, "Invalid key") // validate HMAC - guard characteristic.authentication.isAuthenticated(with: secret) + guard characteristic.authentication.isAuthenticated(using: secret) else { print("Invalid key secret"); return } // guard against replay attacks @@ -524,63 +525,66 @@ public final class LockGATTServiceController : return } - print("Key \(key.identifier) \(key.name) requested keys list") + print("Key \(key.id) \(key.name) requested keys list") // send list via notifications let list = authorization.list let notifications = KeyListNotification.from(list: list) let notificationChunks = try notifications.map { - ($0, try KeysCharacteristic.from($0, sharedSecret: secret, maximumUpdateValueLength: maximumUpdateValueLength)) + ($0, try KeysCharacteristic.from($0, id: key.id, key: secret, maximumUpdateValueLength: maximumUpdateValueLength)) } // write to characteristic and issue notifications for (notification, chunks) in notificationChunks { for (index, chunk) in chunks.enumerated() { - peripheral[characteristic: keysResponseHandle] = chunk.data - print("Sent chunk \(index + 1) for \(notification.key.identifier) (\(chunk.data.count) bytes)") - usleep(100) + await peripheral.write(chunk.data, forCharacteristic: keysResponseHandle) + print("Sent chunk \(index + 1) for \(notification.key.id) (\(chunk.data.count) bytes)") + try await Task.sleep(nanoseconds: 10_000_000) } } - print("Key \(key.identifier) \(key.name) recieved keys list") + print("Key \(key.id) \(key.name) recieved keys list") } catch { print("List keys error: \(error)") } } - private func listEventsRequest(_ characteristic: ListEventsCharacteristic, maximumUpdateValueLength: Int) { + private func listEventsRequest(_ characteristic: ListEventsCharacteristic, maximumUpdateValueLength: Int) async { - let keyIdentifier = characteristic.identifier + let keyIdentifier = characteristic.encryptedData.authentication.message.id do { guard let (key, secret) = try authorization.key(for: keyIdentifier) else { print("Unknown key \(keyIdentifier)"); return } - assert(key.identifier == keyIdentifier, "Invalid key") + assert(key.id == keyIdentifier, "Invalid key") // validate HMAC - guard characteristic.authentication.isAuthenticated(with: secret) + guard characteristic.encryptedData.authentication.isAuthenticated(using: secret) else { print("Invalid key secret"); return } // guard against replay attacks - let timestamp = characteristic.authentication.message.date + let timestamp = characteristic.encryptedData.authentication.message.date let now = Date() guard timestamp <= now + authorizationTimeout, // cannot be used later for replay attacks timestamp > now - authorizationTimeout // only valid for 5 seconds else { print("Authentication expired \(timestamp) < \(now)"); return } - print("Key \(key.identifier) \(key.name) requested events list") + print("Key \(key.id) \(key.name) requested events list") + + // decrypt + let request = try characteristic.decrypt(using: secret) - if let fetchRequest = characteristic.fetchRequest { + if let fetchRequest = request.fetchRequest { dump(fetchRequest) } - var fetchRequest = characteristic.fetchRequest ?? .init() + var fetchRequest = request.fetchRequest ?? .init() // enforce permission, non-administrators can only view their own events. if key.permission.isAdministrator == false { var predicate = fetchRequest.predicate ?? .empty - predicate.keys = [key.identifier] + predicate.keys = [key.id] fetchRequest.predicate = predicate } @@ -588,19 +592,19 @@ public final class LockGATTServiceController : let list = try events.fetch(fetchRequest) let notifications = EventListNotification.from(list: list) let notificationChunks = try notifications.map { - ($0, try EventsCharacteristic.from($0, sharedSecret: secret, maximumUpdateValueLength: maximumUpdateValueLength)) + ($0, try EventsCharacteristic.from($0, id: key.id, key: secret, maximumUpdateValueLength: maximumUpdateValueLength)) } // write to characteristic and issue notifications for (notification, chunks) in notificationChunks { for (index, chunk) in chunks.enumerated() { - peripheral[characteristic: eventsResponseHandle] = chunk.data - print("Sent chunk \(index + 1)\(notification.event.flatMap({ " for event \($0.identifier)" }) ?? "") (\(chunk.data.count) bytes)") - usleep(100) + await peripheral.write(chunk.data, forCharacteristic: eventsResponseHandle) + print("Sent chunk \(index + 1)\(notification.event.flatMap({ " for event \($0.id)" }) ?? "") (\(chunk.data.count) bytes)") + try await Task.sleep(nanoseconds: 10_000_000) } } - print("Key \(key.identifier) \(key.name) recieved events list") + print("Key \(key.id) \(key.name) recieved events list") } catch { print("List keys error: \(error)") } } @@ -615,7 +619,6 @@ public protocol UnlockDelegate { public struct UnlockSimulator: UnlockDelegate { public func unlock(_ action: UnlockAction) throws { - print("Simulate unlock with action \(action)") } } From 064bb5df2f9f26d04fd1d52b0638632a49ac84ea Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 14:43:53 -0700 Subject: [PATCH 027/229] Updated unit tests --- Tests/CoreLockTests/GATTProfileTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/CoreLockTests/GATTProfileTests.swift b/Tests/CoreLockTests/GATTProfileTests.swift index 5848b419..cbb9c560 100644 --- a/Tests/CoreLockTests/GATTProfileTests.swift +++ b/Tests/CoreLockTests/GATTProfileTests.swift @@ -35,7 +35,7 @@ final class GATTProfileTests: XCTestCase { let characteristic = try UnlockCharacteristic(request: request, using: key.secret, id: key.id) guard let decodedCharacteristic = UnlockCharacteristic(data: characteristic.data) else { XCTFail("Could not parse bytes"); return } - let decodedRequest = try decodedCharacteristic.decrypt(with: key.secret) + let decodedRequest = try decodedCharacteristic.decrypt(using: key.secret) XCTAssertEqual(decodedRequest, request) XCTAssertEqual(characteristic, decodedCharacteristic) XCTAssert(decodedCharacteristic.encryptedData.authentication.isAuthenticated(using: key.secret)) From e8986bc5be42fc3d0d9a447eaf1469fb9aeb1204 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 15:15:03 -0700 Subject: [PATCH 028/229] Updated `lockd` for Swift 5.7 --- Package.swift | 37 +++- Sources/lockd/Extensions/CoreBluetooth.swift | 44 ++++ Sources/lockd/LockDaemon.swift | 204 +++++++++++++++++++ Sources/lockd/main.swift | 203 ------------------ 4 files changed, 279 insertions(+), 209 deletions(-) create mode 100644 Sources/lockd/Extensions/CoreBluetooth.swift create mode 100644 Sources/lockd/LockDaemon.swift delete mode 100644 Sources/lockd/main.swift diff --git a/Package.swift b/Package.swift index d148760f..2ddac1b7 100644 --- a/Package.swift +++ b/Package.swift @@ -43,21 +43,46 @@ let package = Package( ) ], targets: [ - /* - .target( + .executableTarget( name: "lockd", dependencies: [ + .product( + name: "Bluetooth", + package: "Bluetooth" + ), + .product( + name: "BluetoothGATT", + package: "Bluetooth", + condition: .when(platforms: [.macOS, .linux]) + ), + .product( + name: "BluetoothHCI", + package: "Bluetooth", + condition: .when(platforms: [.macOS, .linux]) + ), + .product( + name: "BluetoothGAP", + package: "Bluetooth", + condition: .when(platforms: [.macOS, .linux]) + ), + .product( + name: "DarwinGATT", + package: "GATT", + condition: .when(platforms: [.macOS]) + ), "CoreLockGATTServer", - "SwiftyGPIO", - "CoreLockWebServer" + "SwiftyGPIO" ] - ),*/ + ), .target( name: "CoreLock", dependencies: [ "TLVCoding", "GATT", - .product(name: "Bluetooth", package: "Bluetooth"), + .product( + name: "Bluetooth", + package: "Bluetooth" + ), ] ), .target( diff --git a/Sources/lockd/Extensions/CoreBluetooth.swift b/Sources/lockd/Extensions/CoreBluetooth.swift new file mode 100644 index 00000000..ebc5dd1a --- /dev/null +++ b/Sources/lockd/Extensions/CoreBluetooth.swift @@ -0,0 +1,44 @@ +// +// CoreBluetooth.swift +// +// +// Created by Alsey Coleman Miller on 5/10/20. +// + +#if canImport(CoreBluetooth) +import Foundation +import CoreBluetooth +import Bluetooth +import GATT +import DarwinGATT + +internal protocol CoreBluetoothManager { + + var state: DarwinBluetoothState { get async } +} + +extension DarwinPeripheral: CoreBluetoothManager { } +extension DarwinCentral: CoreBluetoothManager { } + +extension CoreBluetoothManager { + + /// Wait for CoreBluetooth to be ready. + func waitPowerOn(warning: Int = 3, timeout: Int = 10) async throws { + + var powerOnWait = 0 + while await state != .poweredOn { + + // inform user after 3 seconds + if powerOnWait == warning { + print("Waiting for CoreBluetooth to be ready, please turn on Bluetooth") + } + + try await Task.sleep(nanoseconds: 1_000_000_000) + powerOnWait += 1 + guard powerOnWait < timeout + else { throw BTLEAgentError.bluetoothUnavailable } + } + } +} +#endif + diff --git a/Sources/lockd/LockDaemon.swift b/Sources/lockd/LockDaemon.swift new file mode 100644 index 00000000..e907e755 --- /dev/null +++ b/Sources/lockd/LockDaemon.swift @@ -0,0 +1,204 @@ +// +// main.swift +// lockd +// +// Created by Alsey Coleman Miller on 8/11/18. +// + +#if os(Linux) +import Glibc +import BluetoothLinux +#elseif os(macOS) +import Darwin +import DarwinGATT +#endif + +import Foundation +import CoreFoundation +import Dispatch + +import Bluetooth +import GATT +import CoreLock +import CoreLockGATTServer +//import CoreLockWebServer + +#if os(Linux) +typealias LinuxCentral = GATTCentral +typealias LinuxPeripheral = GATTPeripheral +typealias NativeCentral = LinuxCentral +typealias NativePeripheral = LinuxPeripheral +#elseif os(macOS) +typealias NativeCentral = DarwinCentral +typealias NativePeripheral = DarwinPeripheral +#else +#error("Unsupported platform") +#endif + +/// Lock Daemon +@main +struct LockDaemon { + + static var id: UUID { controller.lockServiceController.configurationStore.configuration.id } + private static var controller: LockGATTController! + private static var hostController: BluetoothHostControllerInterface? + private static var gpio: LockGPIOController? + //static let webServer = LockWebServer() + + static func main() { + + // start async code + Task { + do { + try await start() + } + catch { + fatalError("\(error)") + } + } + + // run main loop + RunLoop.current.run() + } + + private static func start() async throws { + + #if os(Linux) + hostController = await HostController.default + // keep trying to load Bluetooth device + while hostController == nil { + print("No Bluetooth adapters found") + try await Task.sleep(nanoseconds: 5 * 1_000_000_000) + hostController = await HostController.default + } + + let address = try await hostController!.readDeviceAddress() + print("Bluetooth Controller: \(address)") + let serverOptions = GATTPeripheralOptions( + maximumTransmissionUnit: .max, + maximumPreparedWrites: 1000 + ) + let peripheral = LinuxPeripheral( + hostController: hostController, + options: serverOptions, + socket: BluetoothLinux.L2CAPSocket.self + ) + #elseif os(macOS) + let peripheral = DarwinPeripheral() + #endif + + print("Initialized \(String(reflecting: type(of: peripheral))) with options:") + dump(peripheral.options) + + peripheral.log = { print("Peripheral:", $0) } + + #if os(macOS) + // wait until XPC connection to blued is established and hardware is on + try await peripheral.waitPowerOn() + #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.id + + print("🔒 Lock \(lockIdentifier)") + + // configure Smart Connect BLE Controller + controller = try await LockGATTController(peripheral: peripheral) + controller?.lockServiceController.configurationStore = configurationStore + 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 + } + } + */ + + // 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) + + await controller?.setHardware(hardware) + //webServer.hardware = hardware + + // load GPIO + if let gpioController = hardware.gpioController() { + print("Loaded GPIO Controller: \(type(of: gpioController))") + controller?.lockServiceController.unlockDelegate = gpioController + gpio = gpioController + gpioController.didPressResetButton = { + print("Reset Button pressed at \(Date())") + Task { await controller?.lockServiceController.reset() } + } + } + } + + // publish GATT server, enable advertising + try await peripheral.start() + + // configure custom advertising + try await hostController?.setLockAdvertisingData(lock: lockIdentifier, rssi: 30) // FIXME: RSSI + try await hostController?.setLockScanResponse() + try await hostController?.writeLocalName("Lock") + + controller?.lockServiceController.lockChanged = lockChanged + //webServer.lockChanged = lockChanged + + // make sure the device is always discoverable + Task.detached { + while controller != nil { + try await Task.sleep(nanoseconds: 30 * 1_000_000_000) + do { try await hostController?.enableLowEnergyAdvertising() } + catch HCIError.commandDisallowed { } // already enabled + catch { + print("Unable to enable advertising") + dump(error) + } + } + } + } + + // change advertisment for notifications + private static func lockChanged() { + guard let hostController = self.hostController else { + return + } + Task.detached { + do { + try await hostController.setNotificationAdvertisement(rssi: 30) // FIXME: RSSI + try await Task.sleep(nanoseconds: 3 * 1_000_000_000) + try await hostController.setLockAdvertisingData(lock: id, rssi: 30) + } + catch { + print("Unable to change advertising") + dump(error) + } + } + } +} diff --git a/Sources/lockd/main.swift b/Sources/lockd/main.swift deleted file mode 100644 index c8890e5c..00000000 --- a/Sources/lockd/main.swift +++ /dev/null @@ -1,203 +0,0 @@ -// -// main.swift -// lockd -// -// Created by Alsey Coleman Miller on 8/11/18. -// - -#if os(Linux) -import Glibc -import BluetoothLinux -#elseif os(macOS) -import Darwin -import BluetoothDarwin -import DarwinGATT -#endif - -import Foundation -import CoreFoundation -import Dispatch - -import Bluetooth -import GATT -import CoreLock -import CoreLockGATTServer -import CoreLockWebServer - -#if os(Linux) -typealias LinuxPeripheral = GATTPeripheral -var controller: LockGATTController? -#elseif os(macOS) -var controller: LockGATTController? -#endif - -let webServer = LockWebServer() - -var gpio: LockGPIOController? -var advertiseTimer: Timer? -let backgroundQueue = DispatchQueue(label: "com.colemancda.lockd") - -func run() throws { - - guard let hostController = HostController.default - else { throw LockGATTServerError.bluetoothUnavailible } - - let address = try hostController.readDeviceAddress() - - print("Bluetooth Controller: \(address)") - - func peripheralLog(_ message: String) { - print("Peripheral:", message) - } - - #if os(Linux) - let serverSocket = try L2CAPSocket.lowEnergyServer( - controllerAddress: address, - isRandom: false, - securityLevel: .low - ) - let options = GATTPeripheralOptions( - maximumTransmissionUnit: .max, - maximumPreparedWrites: 1000 - ) - let peripheral = LinuxPeripheral(controller: hostController, options: options) - peripheral.newConnection = { - let socket = try serverSocket.waitForConnection() - let central = Central(identifier: socket.address) - peripheralLog("[\(central)]: New \(socket.addressType) connection") - return (socket, central) - } - #elseif os(macOS) - let peripheral = DarwinPeripheral() - #endif - - print("Initialized \(String(reflecting: type(of: peripheral))) with options:") - dump(peripheral.options) - - peripheral.log = peripheralLog - - #if os(macOS) - // wait until XPC connection to blued is established and hardware is on - 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)") - - // configure Smart Connect BLE Controller - controller = try LockGATTController(peripheral: peripheral) - controller?.lockServiceController.configurationStore = configurationStore - 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 - } - } - - // 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() { - print("Loaded GPIO Controller: \(type(of: gpioController))") - controller?.lockServiceController.unlockDelegate = gpioController - gpio = gpioController - gpioController.didPressResetButton = { - print("Reset Button pressed at \(Date())") - controller?.lockServiceController.reset() - } - } - } - - // publish GATT server, enable advertising - try peripheral.start() - - // configure custom advertising - try hostController.setLockAdvertisingData(lock: lockIdentifier, rssi: 30) // FIXME: RSSI - try hostController.setLockScanResponse() - try hostController.writeLocalName("Lock") - - // change advertisment for notifications - func lockChanged() { - backgroundQueue.asyncAfter(deadline: .now() + 2) { - do { - try hostController.setNotificationAdvertisement(rssi: 30) // FIXME: RSSI - sleep(5) - try hostController.setLockAdvertisingData(lock: lockIdentifier, rssi: 30) - } - catch { - print("Unable to change advertising") - dump(error) - } - } - } - - 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 - backgroundQueue.async { - do { try hostController.enableLowEnergyAdvertising() } - catch HCIError.commandDisallowed { } // already enabled - catch { - print("Unable to enable advertising") - dump(error) - } - } - } - } - - // start web server - DispatchQueue.global(qos: .userInitiated).async { - webServer.run() - } - - // run main loop - RunLoop.main.run() -} - -func Error(_ text: String) -> Never { - print("Exiting with error:", text) - exit(EXIT_FAILURE) -} - -do { try run() } -catch { - dump(error) - Error("\(error.localizedDescription)") -} From 559c8ae934e2d14d6e47d766d875e6532b98ea20 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 15:19:48 -0700 Subject: [PATCH 029/229] Build `lockd` --- Package.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 2ddac1b7..ccb7b23c 100644 --- a/Package.swift +++ b/Package.swift @@ -10,11 +10,10 @@ let package = Package( .tvOS(.v13), ], products: [ - /* .executable( name: "lockd", targets: ["lockd"] - ),*/ + ), .library( name: "CoreLock", targets: ["CoreLock"] From b119a7dd17b3e9739993c8477e86db5772518ed0 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 15:48:32 -0700 Subject: [PATCH 030/229] [CoreLock] Fixed iOS support --- Sources/CoreLock/Bluetooth/GATTProfile.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CoreLock/Bluetooth/GATTProfile.swift b/Sources/CoreLock/Bluetooth/GATTProfile.swift index 571e1cb7..b00c2a49 100644 --- a/Sources/CoreLock/Bluetooth/GATTProfile.swift +++ b/Sources/CoreLock/Bluetooth/GATTProfile.swift @@ -33,7 +33,7 @@ public protocol GATTProfileService { } /// GATT Service Characteristic -public protocol GATTProfileCharacteristic: GATTCharacteristic { +public protocol GATTProfileCharacteristic { static var service: GATTProfileService.Type { get } From d11dd076c33446716d3991986e62fed951d2ed7f Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 15:48:57 -0700 Subject: [PATCH 031/229] [CoreLock] Updated Xcode project --- Xcode/CoreLock.xcodeproj/project.pbxproj | 1381 ++++++++--------- .../contents.xcworkspacedata | 2 +- .../xcshareddata/swiftpm/Package.resolved | 32 + 3 files changed, 695 insertions(+), 720 deletions(-) create mode 100644 Xcode/CoreLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/Xcode/CoreLock.xcodeproj/project.pbxproj b/Xcode/CoreLock.xcodeproj/project.pbxproj index 778a8194..6f33165b 100644 --- a/Xcode/CoreLock.xcodeproj/project.pbxproj +++ b/Xcode/CoreLock.xcodeproj/project.pbxproj @@ -7,263 +7,263 @@ objects = { /* Begin PBXBuildFile section */ - 6E0A32D522D8FCFB002EF9DE /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0A32D422D8FCFB002EF9DE /* URL.swift */; }; - 6E0A32D622D8FCFB002EF9DE /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0A32D422D8FCFB002EF9DE /* URL.swift */; }; - 6E0A32D722D8FCFB002EF9DE /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0A32D422D8FCFB002EF9DE /* URL.swift */; }; - 6E0A32D822D8FCFB002EF9DE /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0A32D422D8FCFB002EF9DE /* URL.swift */; }; + 6E01DB7528D683AB004B5956 /* LockAuthorizationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3428D683AA004B5956 /* LockAuthorizationStore.swift */; }; + 6E01DB7628D683AB004B5956 /* LockAuthorizationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3428D683AA004B5956 /* LockAuthorizationStore.swift */; }; + 6E01DB7728D683AB004B5956 /* LockAuthorizationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3428D683AA004B5956 /* LockAuthorizationStore.swift */; }; + 6E01DB7828D683AB004B5956 /* LockAuthorizationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3428D683AA004B5956 /* LockAuthorizationStore.swift */; }; + 6E01DB7928D683AB004B5956 /* UnlockAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3528D683AA004B5956 /* UnlockAction.swift */; }; + 6E01DB7A28D683AB004B5956 /* UnlockAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3528D683AA004B5956 /* UnlockAction.swift */; }; + 6E01DB7B28D683AB004B5956 /* UnlockAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3528D683AA004B5956 /* UnlockAction.swift */; }; + 6E01DB7C28D683AB004B5956 /* UnlockAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3528D683AA004B5956 /* UnlockAction.swift */; }; + 6E01DB7D28D683AB004B5956 /* Advertisement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3728D683AA004B5956 /* Advertisement.swift */; }; + 6E01DB7E28D683AB004B5956 /* Advertisement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3728D683AA004B5956 /* Advertisement.swift */; }; + 6E01DB7F28D683AB004B5956 /* Advertisement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3728D683AA004B5956 /* Advertisement.swift */; }; + 6E01DB8028D683AB004B5956 /* Advertisement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3728D683AA004B5956 /* Advertisement.swift */; }; + 6E01DB8128D683AB004B5956 /* ConfirmNewKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3828D683AA004B5956 /* ConfirmNewKeyCharacteristic.swift */; }; + 6E01DB8228D683AB004B5956 /* ConfirmNewKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3828D683AA004B5956 /* ConfirmNewKeyCharacteristic.swift */; }; + 6E01DB8328D683AB004B5956 /* ConfirmNewKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3828D683AA004B5956 /* ConfirmNewKeyCharacteristic.swift */; }; + 6E01DB8428D683AB004B5956 /* ConfirmNewKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3828D683AA004B5956 /* ConfirmNewKeyCharacteristic.swift */; }; + 6E01DB8528D683AB004B5956 /* Central.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3928D683AA004B5956 /* Central.swift */; }; + 6E01DB8628D683AB004B5956 /* Central.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3928D683AA004B5956 /* Central.swift */; }; + 6E01DB8728D683AB004B5956 /* Central.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3928D683AA004B5956 /* Central.swift */; }; + 6E01DB8828D683AB004B5956 /* Central.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3928D683AA004B5956 /* Central.swift */; }; + 6E01DB8928D683AB004B5956 /* ListEventsCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3A28D683AA004B5956 /* ListEventsCharacteristic.swift */; }; + 6E01DB8A28D683AB004B5956 /* ListEventsCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3A28D683AA004B5956 /* ListEventsCharacteristic.swift */; }; + 6E01DB8B28D683AB004B5956 /* ListEventsCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3A28D683AA004B5956 /* ListEventsCharacteristic.swift */; }; + 6E01DB8C28D683AB004B5956 /* ListEventsCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3A28D683AA004B5956 /* ListEventsCharacteristic.swift */; }; + 6E01DB8D28D683AB004B5956 /* UnlockCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3B28D683AA004B5956 /* UnlockCharacteristic.swift */; }; + 6E01DB8E28D683AB004B5956 /* UnlockCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3B28D683AA004B5956 /* UnlockCharacteristic.swift */; }; + 6E01DB8F28D683AB004B5956 /* UnlockCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3B28D683AA004B5956 /* UnlockCharacteristic.swift */; }; + 6E01DB9028D683AB004B5956 /* UnlockCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3B28D683AA004B5956 /* UnlockCharacteristic.swift */; }; + 6E01DB9128D683AB004B5956 /* SetupCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3C28D683AA004B5956 /* SetupCharacteristic.swift */; }; + 6E01DB9228D683AB004B5956 /* SetupCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3C28D683AA004B5956 /* SetupCharacteristic.swift */; }; + 6E01DB9328D683AB004B5956 /* SetupCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3C28D683AA004B5956 /* SetupCharacteristic.swift */; }; + 6E01DB9428D683AB004B5956 /* SetupCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3C28D683AA004B5956 /* SetupCharacteristic.swift */; }; + 6E01DB9528D683AB004B5956 /* DeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3D28D683AA004B5956 /* DeviceManager.swift */; }; + 6E01DB9628D683AB004B5956 /* DeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3D28D683AA004B5956 /* DeviceManager.swift */; }; + 6E01DB9728D683AB004B5956 /* DeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3D28D683AA004B5956 /* DeviceManager.swift */; }; + 6E01DB9828D683AB004B5956 /* DeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3D28D683AA004B5956 /* DeviceManager.swift */; }; + 6E01DB9928D683AB004B5956 /* LockInformationCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3E28D683AA004B5956 /* LockInformationCharacteristic.swift */; }; + 6E01DB9A28D683AB004B5956 /* LockInformationCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3E28D683AA004B5956 /* LockInformationCharacteristic.swift */; }; + 6E01DB9B28D683AB004B5956 /* LockInformationCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3E28D683AA004B5956 /* LockInformationCharacteristic.swift */; }; + 6E01DB9C28D683AB004B5956 /* LockInformationCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3E28D683AA004B5956 /* LockInformationCharacteristic.swift */; }; + 6E01DB9D28D683AB004B5956 /* ListKeysCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3F28D683AA004B5956 /* ListKeysCharacteristic.swift */; }; + 6E01DB9E28D683AB004B5956 /* ListKeysCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3F28D683AA004B5956 /* ListKeysCharacteristic.swift */; }; + 6E01DB9F28D683AB004B5956 /* ListKeysCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3F28D683AA004B5956 /* ListKeysCharacteristic.swift */; }; + 6E01DBA028D683AB004B5956 /* ListKeysCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB3F28D683AA004B5956 /* ListKeysCharacteristic.swift */; }; + 6E01DBA128D683AB004B5956 /* KeysCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4028D683AA004B5956 /* KeysCharacteristic.swift */; }; + 6E01DBA228D683AB004B5956 /* KeysCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4028D683AA004B5956 /* KeysCharacteristic.swift */; }; + 6E01DBA328D683AB004B5956 /* KeysCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4028D683AA004B5956 /* KeysCharacteristic.swift */; }; + 6E01DBA428D683AB004B5956 /* KeysCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4028D683AA004B5956 /* KeysCharacteristic.swift */; }; + 6E01DBA528D683AB004B5956 /* CreateNewKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4128D683AA004B5956 /* CreateNewKeyCharacteristic.swift */; }; + 6E01DBA628D683AB004B5956 /* CreateNewKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4128D683AA004B5956 /* CreateNewKeyCharacteristic.swift */; }; + 6E01DBA728D683AB004B5956 /* CreateNewKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4128D683AA004B5956 /* CreateNewKeyCharacteristic.swift */; }; + 6E01DBA828D683AB004B5956 /* CreateNewKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4128D683AA004B5956 /* CreateNewKeyCharacteristic.swift */; }; + 6E01DBA928D683AB004B5956 /* TLV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4228D683AA004B5956 /* TLV.swift */; }; + 6E01DBAA28D683AB004B5956 /* TLV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4228D683AA004B5956 /* TLV.swift */; }; + 6E01DBAB28D683AB004B5956 /* TLV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4228D683AA004B5956 /* TLV.swift */; }; + 6E01DBAC28D683AB004B5956 /* TLV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4228D683AA004B5956 /* TLV.swift */; }; + 6E01DBAD28D683AB004B5956 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4328D683AA004B5956 /* Notification.swift */; }; + 6E01DBAE28D683AB004B5956 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4328D683AA004B5956 /* Notification.swift */; }; + 6E01DBAF28D683AB004B5956 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4328D683AA004B5956 /* Notification.swift */; }; + 6E01DBB028D683AB004B5956 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4328D683AA004B5956 /* Notification.swift */; }; + 6E01DBB128D683AB004B5956 /* EventsCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4428D683AA004B5956 /* EventsCharacteristic.swift */; }; + 6E01DBB228D683AB004B5956 /* EventsCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4428D683AA004B5956 /* EventsCharacteristic.swift */; }; + 6E01DBB328D683AB004B5956 /* EventsCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4428D683AA004B5956 /* EventsCharacteristic.swift */; }; + 6E01DBB428D683AB004B5956 /* EventsCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4428D683AA004B5956 /* EventsCharacteristic.swift */; }; + 6E01DBB528D683AB004B5956 /* GATTProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4528D683AA004B5956 /* GATTProfile.swift */; }; + 6E01DBB628D683AB004B5956 /* GATTProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4528D683AA004B5956 /* GATTProfile.swift */; }; + 6E01DBB728D683AB004B5956 /* GATTProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4528D683AA004B5956 /* GATTProfile.swift */; }; + 6E01DBB828D683AB004B5956 /* GATTProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4528D683AA004B5956 /* GATTProfile.swift */; }; + 6E01DBB928D683AB004B5956 /* RemoveKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4628D683AA004B5956 /* RemoveKeyCharacteristic.swift */; }; + 6E01DBBA28D683AB004B5956 /* RemoveKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4628D683AA004B5956 /* RemoveKeyCharacteristic.swift */; }; + 6E01DBBB28D683AB004B5956 /* RemoveKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4628D683AA004B5956 /* RemoveKeyCharacteristic.swift */; }; + 6E01DBBC28D683AB004B5956 /* RemoveKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4628D683AA004B5956 /* RemoveKeyCharacteristic.swift */; }; + 6E01DBBD28D683AB004B5956 /* GATTError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4728D683AA004B5956 /* GATTError.swift */; }; + 6E01DBBE28D683AB004B5956 /* GATTError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4728D683AA004B5956 /* GATTError.swift */; }; + 6E01DBBF28D683AB004B5956 /* GATTError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4728D683AA004B5956 /* GATTError.swift */; }; + 6E01DBC028D683AB004B5956 /* GATTError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4728D683AA004B5956 /* GATTError.swift */; }; + 6E01DBC128D683AB004B5956 /* LockInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4828D683AA004B5956 /* LockInformation.swift */; }; + 6E01DBC228D683AB004B5956 /* LockInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4828D683AA004B5956 /* LockInformation.swift */; }; + 6E01DBC328D683AB004B5956 /* LockInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4828D683AA004B5956 /* LockInformation.swift */; }; + 6E01DBC428D683AB004B5956 /* LockInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4828D683AA004B5956 /* LockInformation.swift */; }; + 6E01DBC528D683AB004B5956 /* LockModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4928D683AA004B5956 /* LockModel.swift */; }; + 6E01DBC628D683AB004B5956 /* LockModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4928D683AA004B5956 /* LockModel.swift */; }; + 6E01DBC728D683AB004B5956 /* LockModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4928D683AA004B5956 /* LockModel.swift */; }; + 6E01DBC828D683AB004B5956 /* LockModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4928D683AA004B5956 /* LockModel.swift */; }; + 6E01DBC928D683AB004B5956 /* BuildVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4A28D683AA004B5956 /* BuildVersion.swift */; }; + 6E01DBCA28D683AB004B5956 /* BuildVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4A28D683AA004B5956 /* BuildVersion.swift */; }; + 6E01DBCB28D683AB004B5956 /* BuildVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4A28D683AA004B5956 /* BuildVersion.swift */; }; + 6E01DBCC28D683AB004B5956 /* BuildVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4A28D683AA004B5956 /* BuildVersion.swift */; }; + 6E01DBCD28D683AB004B5956 /* KeyCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4B28D683AA004B5956 /* KeyCredentials.swift */; }; + 6E01DBCE28D683AB004B5956 /* KeyCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4B28D683AA004B5956 /* KeyCredentials.swift */; }; + 6E01DBCF28D683AB004B5956 /* KeyCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4B28D683AA004B5956 /* KeyCredentials.swift */; }; + 6E01DBD028D683AB004B5956 /* KeyCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4B28D683AA004B5956 /* KeyCredentials.swift */; }; + 6E01DBD128D683AB004B5956 /* POSIXTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4C28D683AA004B5956 /* POSIXTime.swift */; }; + 6E01DBD228D683AB004B5956 /* POSIXTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4C28D683AA004B5956 /* POSIXTime.swift */; }; + 6E01DBD328D683AB004B5956 /* POSIXTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4C28D683AA004B5956 /* POSIXTime.swift */; }; + 6E01DBD428D683AB004B5956 /* POSIXTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4C28D683AA004B5956 /* POSIXTime.swift */; }; + 6E01DBD528D683AB004B5956 /* EventStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4D28D683AA004B5956 /* EventStore.swift */; }; + 6E01DBD628D683AB004B5956 /* EventStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4D28D683AA004B5956 /* EventStore.swift */; }; + 6E01DBD728D683AB004B5956 /* EventStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4D28D683AA004B5956 /* EventStore.swift */; }; + 6E01DBD828D683AB004B5956 /* EventStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4D28D683AA004B5956 /* EventStore.swift */; }; + 6E01DBD928D683AB004B5956 /* GitCommits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4E28D683AA004B5956 /* GitCommits.swift */; }; + 6E01DBDA28D683AB004B5956 /* GitCommits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4E28D683AA004B5956 /* GitCommits.swift */; }; + 6E01DBDB28D683AB004B5956 /* GitCommits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4E28D683AA004B5956 /* GitCommits.swift */; }; + 6E01DBDC28D683AB004B5956 /* GitCommits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4E28D683AA004B5956 /* GitCommits.swift */; }; + 6E01DBDD28D683AB004B5956 /* Chunk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4F28D683AA004B5956 /* Chunk.swift */; }; + 6E01DBDE28D683AB004B5956 /* Chunk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4F28D683AA004B5956 /* Chunk.swift */; }; + 6E01DBDF28D683AB004B5956 /* Chunk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4F28D683AA004B5956 /* Chunk.swift */; }; + 6E01DBE028D683AB004B5956 /* Chunk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB4F28D683AA004B5956 /* Chunk.swift */; }; + 6E01DBE128D683AB004B5956 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5028D683AA004B5956 /* Status.swift */; }; + 6E01DBE228D683AB004B5956 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5028D683AA004B5956 /* Status.swift */; }; + 6E01DBE328D683AB004B5956 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5028D683AA004B5956 /* Status.swift */; }; + 6E01DBE428D683AB004B5956 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5028D683AA004B5956 /* Status.swift */; }; + 6E01DBE528D683AB004B5956 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5128D683AA004B5956 /* Version.swift */; }; + 6E01DBE628D683AB004B5956 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5128D683AA004B5956 /* Version.swift */; }; + 6E01DBE728D683AB004B5956 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5128D683AA004B5956 /* Version.swift */; }; + 6E01DBE828D683AB004B5956 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5128D683AA004B5956 /* Version.swift */; }; + 6E01DBE928D683AB004B5956 /* SmartLockProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5228D683AA004B5956 /* SmartLockProfile.swift */; }; + 6E01DBEA28D683AB004B5956 /* SmartLockProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5228D683AA004B5956 /* SmartLockProfile.swift */; }; + 6E01DBEB28D683AB004B5956 /* SmartLockProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5228D683AA004B5956 /* SmartLockProfile.swift */; }; + 6E01DBEC28D683AB004B5956 /* SmartLockProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5228D683AA004B5956 /* SmartLockProfile.swift */; }; + 6E01DBED28D683AB004B5956 /* EventsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5428D683AB004B5956 /* EventsRequest.swift */; }; + 6E01DBEE28D683AB004B5956 /* EventsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5428D683AB004B5956 /* EventsRequest.swift */; }; + 6E01DBEF28D683AB004B5956 /* EventsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5428D683AB004B5956 /* EventsRequest.swift */; }; + 6E01DBF028D683AB004B5956 /* EventsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5428D683AB004B5956 /* EventsRequest.swift */; }; + 6E01DBF128D683AB004B5956 /* DeleteKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5528D683AB004B5956 /* DeleteKeyRequest.swift */; }; + 6E01DBF228D683AB004B5956 /* DeleteKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5528D683AB004B5956 /* DeleteKeyRequest.swift */; }; + 6E01DBF328D683AB004B5956 /* DeleteKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5528D683AB004B5956 /* DeleteKeyRequest.swift */; }; + 6E01DBF428D683AB004B5956 /* DeleteKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5528D683AB004B5956 /* DeleteKeyRequest.swift */; }; + 6E01DBF528D683AB004B5956 /* EventsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5628D683AB004B5956 /* EventsResponse.swift */; }; + 6E01DBF628D683AB004B5956 /* EventsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5628D683AB004B5956 /* EventsResponse.swift */; }; + 6E01DBF728D683AB004B5956 /* EventsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5628D683AB004B5956 /* EventsResponse.swift */; }; + 6E01DBF828D683AB004B5956 /* EventsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5628D683AB004B5956 /* EventsResponse.swift */; }; + 6E01DBF928D683AB004B5956 /* CreateNewKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5728D683AB004B5956 /* CreateNewKeyRequest.swift */; }; + 6E01DBFA28D683AB004B5956 /* CreateNewKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5728D683AB004B5956 /* CreateNewKeyRequest.swift */; }; + 6E01DBFB28D683AB004B5956 /* CreateNewKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5728D683AB004B5956 /* CreateNewKeyRequest.swift */; }; + 6E01DBFC28D683AB004B5956 /* CreateNewKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5728D683AB004B5956 /* CreateNewKeyRequest.swift */; }; + 6E01DBFD28D683AB004B5956 /* LockNetService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5828D683AB004B5956 /* LockNetService.swift */; }; + 6E01DBFE28D683AB004B5956 /* LockNetService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5828D683AB004B5956 /* LockNetService.swift */; }; + 6E01DBFF28D683AB004B5956 /* LockNetService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5828D683AB004B5956 /* LockNetService.swift */; }; + 6E01DC0028D683AB004B5956 /* LockNetService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5828D683AB004B5956 /* LockNetService.swift */; }; + 6E01DC0128D683AB004B5956 /* KeysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5928D683AB004B5956 /* KeysResponse.swift */; }; + 6E01DC0228D683AB004B5956 /* KeysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5928D683AB004B5956 /* KeysResponse.swift */; }; + 6E01DC0328D683AB004B5956 /* KeysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5928D683AB004B5956 /* KeysResponse.swift */; }; + 6E01DC0428D683AB004B5956 /* KeysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5928D683AB004B5956 /* KeysResponse.swift */; }; + 6E01DC0528D683AB004B5956 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5A28D683AB004B5956 /* URLSession.swift */; }; + 6E01DC0628D683AB004B5956 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5A28D683AB004B5956 /* URLSession.swift */; }; + 6E01DC0728D683AB004B5956 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5A28D683AB004B5956 /* URLSession.swift */; }; + 6E01DC0828D683AB004B5956 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5A28D683AB004B5956 /* URLSession.swift */; }; + 6E01DC0928D683AB004B5956 /* LockInformationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5B28D683AB004B5956 /* LockInformationResponse.swift */; }; + 6E01DC0A28D683AB004B5956 /* LockInformationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5B28D683AB004B5956 /* LockInformationResponse.swift */; }; + 6E01DC0B28D683AB004B5956 /* LockInformationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5B28D683AB004B5956 /* LockInformationResponse.swift */; }; + 6E01DC0C28D683AB004B5956 /* LockInformationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5B28D683AB004B5956 /* LockInformationResponse.swift */; }; + 6E01DC0D28D683AB004B5956 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5C28D683AB004B5956 /* URL.swift */; }; + 6E01DC0E28D683AB004B5956 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5C28D683AB004B5956 /* URL.swift */; }; + 6E01DC0F28D683AB004B5956 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5C28D683AB004B5956 /* URL.swift */; }; + 6E01DC1028D683AB004B5956 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5C28D683AB004B5956 /* URL.swift */; }; + 6E01DC1128D683AB004B5956 /* UpdateRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5D28D683AB004B5956 /* UpdateRequest.swift */; }; + 6E01DC1228D683AB004B5956 /* UpdateRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5D28D683AB004B5956 /* UpdateRequest.swift */; }; + 6E01DC1328D683AB004B5956 /* UpdateRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5D28D683AB004B5956 /* UpdateRequest.swift */; }; + 6E01DC1428D683AB004B5956 /* UpdateRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5D28D683AB004B5956 /* UpdateRequest.swift */; }; + 6E01DC1528D683AB004B5956 /* KeysRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5E28D683AB004B5956 /* KeysRequest.swift */; }; + 6E01DC1628D683AB004B5956 /* KeysRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5E28D683AB004B5956 /* KeysRequest.swift */; }; + 6E01DC1728D683AB004B5956 /* KeysRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5E28D683AB004B5956 /* KeysRequest.swift */; }; + 6E01DC1828D683AB004B5956 /* KeysRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5E28D683AB004B5956 /* KeysRequest.swift */; }; + 6E01DC1928D683AB004B5956 /* LockInformationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5F28D683AB004B5956 /* LockInformationRequest.swift */; }; + 6E01DC1A28D683AB004B5956 /* LockInformationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5F28D683AB004B5956 /* LockInformationRequest.swift */; }; + 6E01DC1B28D683AB004B5956 /* LockInformationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5F28D683AB004B5956 /* LockInformationRequest.swift */; }; + 6E01DC1C28D683AB004B5956 /* LockInformationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB5F28D683AB004B5956 /* LockInformationRequest.swift */; }; + 6E01DC1D28D683AB004B5956 /* Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6128D683AB004B5956 /* Authentication.swift */; }; + 6E01DC1E28D683AB004B5956 /* Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6128D683AB004B5956 /* Authentication.swift */; }; + 6E01DC1F28D683AB004B5956 /* Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6128D683AB004B5956 /* Authentication.swift */; }; + 6E01DC2028D683AB004B5956 /* Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6128D683AB004B5956 /* Authentication.swift */; }; + 6E01DC2128D683AB004B5956 /* AuthenticationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6228D683AB004B5956 /* AuthenticationError.swift */; }; + 6E01DC2228D683AB004B5956 /* AuthenticationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6228D683AB004B5956 /* AuthenticationError.swift */; }; + 6E01DC2328D683AB004B5956 /* AuthenticationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6228D683AB004B5956 /* AuthenticationError.swift */; }; + 6E01DC2428D683AB004B5956 /* AuthenticationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6228D683AB004B5956 /* AuthenticationError.swift */; }; + 6E01DC2528D683AB004B5956 /* SecureData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6328D683AB004B5956 /* SecureData.swift */; }; + 6E01DC2628D683AB004B5956 /* SecureData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6328D683AB004B5956 /* SecureData.swift */; }; + 6E01DC2728D683AB004B5956 /* SecureData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6328D683AB004B5956 /* SecureData.swift */; }; + 6E01DC2828D683AB004B5956 /* SecureData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6328D683AB004B5956 /* SecureData.swift */; }; + 6E01DC2928D683AB004B5956 /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6428D683AB004B5956 /* Crypto.swift */; }; + 6E01DC2A28D683AB004B5956 /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6428D683AB004B5956 /* Crypto.swift */; }; + 6E01DC2B28D683AB004B5956 /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6428D683AB004B5956 /* Crypto.swift */; }; + 6E01DC2C28D683AB004B5956 /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6428D683AB004B5956 /* Crypto.swift */; }; + 6E01DC2D28D683AB004B5956 /* EncryptedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6528D683AB004B5956 /* EncryptedData.swift */; }; + 6E01DC2E28D683AB004B5956 /* EncryptedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6528D683AB004B5956 /* EncryptedData.swift */; }; + 6E01DC2F28D683AB004B5956 /* EncryptedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6528D683AB004B5956 /* EncryptedData.swift */; }; + 6E01DC3028D683AB004B5956 /* EncryptedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6528D683AB004B5956 /* EncryptedData.swift */; }; + 6E01DC3128D683AB004B5956 /* BitMaskOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6628D683AB004B5956 /* BitMaskOption.swift */; }; + 6E01DC3228D683AB004B5956 /* BitMaskOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6628D683AB004B5956 /* BitMaskOption.swift */; }; + 6E01DC3328D683AB004B5956 /* BitMaskOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6628D683AB004B5956 /* BitMaskOption.swift */; }; + 6E01DC3428D683AB004B5956 /* BitMaskOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6628D683AB004B5956 /* BitMaskOption.swift */; }; + 6E01DC3528D683AB004B5956 /* DateComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6728D683AB004B5956 /* DateComponents.swift */; }; + 6E01DC3628D683AB004B5956 /* DateComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6728D683AB004B5956 /* DateComponents.swift */; }; + 6E01DC3728D683AB004B5956 /* DateComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6728D683AB004B5956 /* DateComponents.swift */; }; + 6E01DC3828D683AB004B5956 /* DateComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6728D683AB004B5956 /* DateComponents.swift */; }; + 6E01DC3928D683AB004B5956 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6928D683AB004B5956 /* Date.swift */; }; + 6E01DC3A28D683AB004B5956 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6928D683AB004B5956 /* Date.swift */; }; + 6E01DC3B28D683AB004B5956 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6928D683AB004B5956 /* Date.swift */; }; + 6E01DC3C28D683AB004B5956 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6928D683AB004B5956 /* Date.swift */; }; + 6E01DC3D28D683AB004B5956 /* UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6A28D683AB004B5956 /* UUID.swift */; }; + 6E01DC3E28D683AB004B5956 /* UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6A28D683AB004B5956 /* UUID.swift */; }; + 6E01DC3F28D683AB004B5956 /* UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6A28D683AB004B5956 /* UUID.swift */; }; + 6E01DC4028D683AB004B5956 /* UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6A28D683AB004B5956 /* UUID.swift */; }; + 6E01DC4128D683AB004B5956 /* Integer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6B28D683AB004B5956 /* Integer.swift */; }; + 6E01DC4228D683AB004B5956 /* Integer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6B28D683AB004B5956 /* Integer.swift */; }; + 6E01DC4328D683AB004B5956 /* Integer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6B28D683AB004B5956 /* Integer.swift */; }; + 6E01DC4428D683AB004B5956 /* Integer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6B28D683AB004B5956 /* Integer.swift */; }; + 6E01DC4528D683AB004B5956 /* Bool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6C28D683AB004B5956 /* Bool.swift */; }; + 6E01DC4628D683AB004B5956 /* Bool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6C28D683AB004B5956 /* Bool.swift */; }; + 6E01DC4728D683AB004B5956 /* Bool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6C28D683AB004B5956 /* Bool.swift */; }; + 6E01DC4828D683AB004B5956 /* Bool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6C28D683AB004B5956 /* Bool.swift */; }; + 6E01DC4928D683AB004B5956 /* Permission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6D28D683AB004B5956 /* Permission.swift */; }; + 6E01DC4A28D683AB004B5956 /* Permission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6D28D683AB004B5956 /* Permission.swift */; }; + 6E01DC4B28D683AB004B5956 /* Permission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6D28D683AB004B5956 /* Permission.swift */; }; + 6E01DC4C28D683AB004B5956 /* Permission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6D28D683AB004B5956 /* Permission.swift */; }; + 6E01DC4D28D683AB004B5956 /* LockState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6E28D683AB004B5956 /* LockState.swift */; }; + 6E01DC4E28D683AB004B5956 /* LockState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6E28D683AB004B5956 /* LockState.swift */; }; + 6E01DC4F28D683AB004B5956 /* LockState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6E28D683AB004B5956 /* LockState.swift */; }; + 6E01DC5028D683AB004B5956 /* LockState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6E28D683AB004B5956 /* LockState.swift */; }; + 6E01DC5128D683AB004B5956 /* NewKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6F28D683AB004B5956 /* NewKey.swift */; }; + 6E01DC5228D683AB004B5956 /* NewKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6F28D683AB004B5956 /* NewKey.swift */; }; + 6E01DC5328D683AB004B5956 /* NewKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6F28D683AB004B5956 /* NewKey.swift */; }; + 6E01DC5428D683AB004B5956 /* NewKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB6F28D683AB004B5956 /* NewKey.swift */; }; + 6E01DC5528D683AB004B5956 /* LockHardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7028D683AB004B5956 /* LockHardware.swift */; }; + 6E01DC5628D683AB004B5956 /* LockHardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7028D683AB004B5956 /* LockHardware.swift */; }; + 6E01DC5728D683AB004B5956 /* LockHardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7028D683AB004B5956 /* LockHardware.swift */; }; + 6E01DC5828D683AB004B5956 /* LockHardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7028D683AB004B5956 /* LockHardware.swift */; }; + 6E01DC5928D683AB004B5956 /* LockConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7128D683AB004B5956 /* LockConfiguration.swift */; }; + 6E01DC5A28D683AB004B5956 /* LockConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7128D683AB004B5956 /* LockConfiguration.swift */; }; + 6E01DC5B28D683AB004B5956 /* LockConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7128D683AB004B5956 /* LockConfiguration.swift */; }; + 6E01DC5C28D683AB004B5956 /* LockConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7128D683AB004B5956 /* LockConfiguration.swift */; }; + 6E01DC5D28D683AB004B5956 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7228D683AB004B5956 /* Event.swift */; }; + 6E01DC5E28D683AB004B5956 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7228D683AB004B5956 /* Event.swift */; }; + 6E01DC5F28D683AB004B5956 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7228D683AB004B5956 /* Event.swift */; }; + 6E01DC6028D683AB004B5956 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7228D683AB004B5956 /* Event.swift */; }; + 6E01DC6128D683AB004B5956 /* Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7328D683AB004B5956 /* Key.swift */; }; + 6E01DC6228D683AB004B5956 /* Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7328D683AB004B5956 /* Key.swift */; }; + 6E01DC6328D683AB004B5956 /* Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7328D683AB004B5956 /* Key.swift */; }; + 6E01DC6428D683AB004B5956 /* Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7328D683AB004B5956 /* Key.swift */; }; + 6E01DC6528D683AB004B5956 /* LockConfigurationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7428D683AB004B5956 /* LockConfigurationStore.swift */; }; + 6E01DC6628D683AB004B5956 /* LockConfigurationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7428D683AB004B5956 /* LockConfigurationStore.swift */; }; + 6E01DC6728D683AB004B5956 /* LockConfigurationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7428D683AB004B5956 /* LockConfigurationStore.swift */; }; + 6E01DC6828D683AB004B5956 /* LockConfigurationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E01DB7428D683AB004B5956 /* LockConfigurationStore.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 */; }; - 6E93D357231C624300119F65 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E93D351231C623B00119F65 /* Notification.swift */; }; - 6E93D358231C624300119F65 /* ListEventsCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E93D352231C623E00119F65 /* ListEventsCharacteristic.swift */; }; - 6E93D359231C624300119F65 /* ListEventsCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E93D352231C623E00119F65 /* ListEventsCharacteristic.swift */; }; - 6E93D35A231C624300119F65 /* ListEventsCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E93D352231C623E00119F65 /* ListEventsCharacteristic.swift */; }; - 6E93D35B231C624300119F65 /* ListEventsCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E93D352231C623E00119F65 /* ListEventsCharacteristic.swift */; }; - 6E93D35C231C624300119F65 /* EventsCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E93D353231C624000119F65 /* EventsCharacteristic.swift */; }; - 6E93D35D231C624300119F65 /* EventsCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E93D353231C624000119F65 /* EventsCharacteristic.swift */; }; - 6E93D35E231C624300119F65 /* EventsCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E93D353231C624000119F65 /* EventsCharacteristic.swift */; }; - 6E93D35F231C624300119F65 /* EventsCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E93D353231C624000119F65 /* EventsCharacteristic.swift */; }; - 6E9794C423301FC400B5C5C9 /* UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9794C323301FC400B5C5C9 /* UUID.swift */; }; - 6E9794C523301FC400B5C5C9 /* UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9794C323301FC400B5C5C9 /* UUID.swift */; }; - 6E9794C623301FC400B5C5C9 /* UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9794C323301FC400B5C5C9 /* UUID.swift */; }; - 6E9794C723301FC400B5C5C9 /* UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E9794C323301FC400B5C5C9 /* UUID.swift */; }; - 6E9C95EA23114962007C18FE /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 6E9C95E923114962007C18FE /* CryptoSwift */; }; 6E9C95ED231149A5007C18FE /* GATT in Frameworks */ = {isa = PBXBuildFile; productRef = 6E9C95EC231149A5007C18FE /* GATT */; }; 6E9C95EF231149A5007C18FE /* DarwinGATT in Frameworks */ = {isa = PBXBuildFile; productRef = 6E9C95EE231149A5007C18FE /* DarwinGATT */; }; 6E9C95F2231149F5007C18FE /* TLVCoding in Frameworks */ = {isa = PBXBuildFile; productRef = 6E9C95F1231149F5007C18FE /* TLVCoding */; }; - 6EACDA99231B353A000CF82A /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EACDA98231B3539000CF82A /* Event.swift */; }; - 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 */; }; 6ED81FE7231662E200B69520 /* TLVCoding in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED81FE6231662E200B69520 /* TLVCoding */; }; - 6ED81FE9231662F100B69520 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED81FE8231662F100B69520 /* CryptoSwift */; }; 6ED81FEB231662F100B69520 /* DarwinGATT in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED81FEA231662F100B69520 /* DarwinGATT */; }; 6ED81FED231662F100B69520 /* GATT in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED81FEC231662F100B69520 /* GATT */; }; 6ED81FEF231662F100B69520 /* TLVCoding in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED81FEE231662F100B69520 /* TLVCoding */; }; - 6ED81FF1231662FB00B69520 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED81FF0231662FB00B69520 /* CryptoSwift */; }; 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 */; }; - 6EF1C65822C9AC9F005E9818 /* SetupCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C62E22C9AC9D005E9818 /* SetupCharacteristic.swift */; }; - 6EF1C65922C9AC9F005E9818 /* LockConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C62F22C9AC9D005E9818 /* LockConfiguration.swift */; }; - 6EF1C65A22C9AC9F005E9818 /* LockConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C62F22C9AC9D005E9818 /* LockConfiguration.swift */; }; - 6EF1C65B22C9AC9F005E9818 /* LockConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C62F22C9AC9D005E9818 /* LockConfiguration.swift */; }; - 6EF1C65C22C9AC9F005E9818 /* LockConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C62F22C9AC9D005E9818 /* LockConfiguration.swift */; }; - 6EF1C65D22C9AC9F005E9818 /* EncryptedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63022C9AC9D005E9818 /* EncryptedData.swift */; }; - 6EF1C65E22C9AC9F005E9818 /* EncryptedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63022C9AC9D005E9818 /* EncryptedData.swift */; }; - 6EF1C65F22C9AC9F005E9818 /* EncryptedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63022C9AC9D005E9818 /* EncryptedData.swift */; }; - 6EF1C66022C9AC9F005E9818 /* EncryptedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63022C9AC9D005E9818 /* EncryptedData.swift */; }; - 6EF1C66122C9AC9F005E9818 /* SecureData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63122C9AC9D005E9818 /* SecureData.swift */; }; - 6EF1C66222C9AC9F005E9818 /* SecureData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63122C9AC9D005E9818 /* SecureData.swift */; }; - 6EF1C66322C9AC9F005E9818 /* SecureData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63122C9AC9D005E9818 /* SecureData.swift */; }; - 6EF1C66422C9AC9F005E9818 /* SecureData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63122C9AC9D005E9818 /* SecureData.swift */; }; - 6EF1C66522C9AC9F005E9818 /* AuthenticationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63222C9AC9D005E9818 /* AuthenticationError.swift */; }; - 6EF1C66622C9AC9F005E9818 /* AuthenticationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63222C9AC9D005E9818 /* AuthenticationError.swift */; }; - 6EF1C66722C9AC9F005E9818 /* AuthenticationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63222C9AC9D005E9818 /* AuthenticationError.swift */; }; - 6EF1C66822C9AC9F005E9818 /* AuthenticationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63222C9AC9D005E9818 /* AuthenticationError.swift */; }; - 6EF1C66922C9AC9F005E9818 /* ListKeysCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63322C9AC9D005E9818 /* ListKeysCharacteristic.swift */; }; - 6EF1C66A22C9AC9F005E9818 /* ListKeysCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63322C9AC9D005E9818 /* ListKeysCharacteristic.swift */; }; - 6EF1C66B22C9AC9F005E9818 /* ListKeysCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63322C9AC9D005E9818 /* ListKeysCharacteristic.swift */; }; - 6EF1C66C22C9AC9F005E9818 /* ListKeysCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63322C9AC9D005E9818 /* ListKeysCharacteristic.swift */; }; - 6EF1C66D22C9AC9F005E9818 /* NewKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63422C9AC9D005E9818 /* NewKey.swift */; }; - 6EF1C66E22C9AC9F005E9818 /* NewKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63422C9AC9D005E9818 /* NewKey.swift */; }; - 6EF1C66F22C9AC9F005E9818 /* NewKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63422C9AC9D005E9818 /* NewKey.swift */; }; - 6EF1C67022C9AC9F005E9818 /* NewKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63422C9AC9D005E9818 /* NewKey.swift */; }; - 6EF1C67122C9AC9F005E9818 /* Advertisement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63522C9AC9D005E9818 /* Advertisement.swift */; }; - 6EF1C67222C9AC9F005E9818 /* Advertisement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63522C9AC9D005E9818 /* Advertisement.swift */; }; - 6EF1C67322C9AC9F005E9818 /* Advertisement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63522C9AC9D005E9818 /* Advertisement.swift */; }; - 6EF1C67422C9AC9F005E9818 /* Advertisement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63522C9AC9D005E9818 /* Advertisement.swift */; }; - 6EF1C67522C9AC9F005E9818 /* BuildVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63622C9AC9D005E9818 /* BuildVersion.swift */; }; - 6EF1C67622C9AC9F005E9818 /* BuildVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63622C9AC9D005E9818 /* BuildVersion.swift */; }; - 6EF1C67722C9AC9F005E9818 /* BuildVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63622C9AC9D005E9818 /* BuildVersion.swift */; }; - 6EF1C67822C9AC9F005E9818 /* BuildVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63622C9AC9D005E9818 /* BuildVersion.swift */; }; - 6EF1C67922C9AC9F005E9818 /* KeysCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63722C9AC9D005E9818 /* KeysCharacteristic.swift */; }; - 6EF1C67A22C9AC9F005E9818 /* KeysCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63722C9AC9D005E9818 /* KeysCharacteristic.swift */; }; - 6EF1C67B22C9AC9F005E9818 /* KeysCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63722C9AC9D005E9818 /* KeysCharacteristic.swift */; }; - 6EF1C67C22C9AC9F005E9818 /* KeysCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63722C9AC9D005E9818 /* KeysCharacteristic.swift */; }; - 6EF1C67D22C9AC9F005E9818 /* Chunk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63822C9AC9D005E9818 /* Chunk.swift */; }; - 6EF1C67E22C9AC9F005E9818 /* Chunk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63822C9AC9D005E9818 /* Chunk.swift */; }; - 6EF1C67F22C9AC9F005E9818 /* Chunk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63822C9AC9D005E9818 /* Chunk.swift */; }; - 6EF1C68022C9AC9F005E9818 /* Chunk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63822C9AC9D005E9818 /* Chunk.swift */; }; - 6EF1C68122C9AC9F005E9818 /* Central.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63922C9AC9D005E9818 /* Central.swift */; }; - 6EF1C68222C9AC9F005E9818 /* Central.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63922C9AC9D005E9818 /* Central.swift */; }; - 6EF1C68322C9AC9F005E9818 /* Central.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63922C9AC9D005E9818 /* Central.swift */; }; - 6EF1C68422C9AC9F005E9818 /* Central.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63922C9AC9D005E9818 /* Central.swift */; }; - 6EF1C68522C9AC9F005E9818 /* GitCommits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63A22C9AC9E005E9818 /* GitCommits.swift */; }; - 6EF1C68622C9AC9F005E9818 /* GitCommits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63A22C9AC9E005E9818 /* GitCommits.swift */; }; - 6EF1C68722C9AC9F005E9818 /* GitCommits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63A22C9AC9E005E9818 /* GitCommits.swift */; }; - 6EF1C68822C9AC9F005E9818 /* GitCommits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63A22C9AC9E005E9818 /* GitCommits.swift */; }; - 6EF1C68922C9AC9F005E9818 /* POSIXTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63B22C9AC9E005E9818 /* POSIXTime.swift */; }; - 6EF1C68A22C9AC9F005E9818 /* POSIXTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63B22C9AC9E005E9818 /* POSIXTime.swift */; }; - 6EF1C68B22C9AC9F005E9818 /* POSIXTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63B22C9AC9E005E9818 /* POSIXTime.swift */; }; - 6EF1C68C22C9AC9F005E9818 /* POSIXTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63B22C9AC9E005E9818 /* POSIXTime.swift */; }; - 6EF1C68D22C9AC9F005E9818 /* CreateNewKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63C22C9AC9E005E9818 /* CreateNewKeyCharacteristic.swift */; }; - 6EF1C68E22C9AC9F005E9818 /* CreateNewKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63C22C9AC9E005E9818 /* CreateNewKeyCharacteristic.swift */; }; - 6EF1C68F22C9AC9F005E9818 /* CreateNewKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63C22C9AC9E005E9818 /* CreateNewKeyCharacteristic.swift */; }; - 6EF1C69022C9AC9F005E9818 /* CreateNewKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63C22C9AC9E005E9818 /* CreateNewKeyCharacteristic.swift */; }; - 6EF1C69122C9AC9F005E9818 /* ConfirmNewKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63D22C9AC9E005E9818 /* ConfirmNewKeyCharacteristic.swift */; }; - 6EF1C69222C9AC9F005E9818 /* ConfirmNewKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63D22C9AC9E005E9818 /* ConfirmNewKeyCharacteristic.swift */; }; - 6EF1C69322C9AC9F005E9818 /* ConfirmNewKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63D22C9AC9E005E9818 /* ConfirmNewKeyCharacteristic.swift */; }; - 6EF1C69422C9AC9F005E9818 /* ConfirmNewKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63D22C9AC9E005E9818 /* ConfirmNewKeyCharacteristic.swift */; }; - 6EF1C69522C9AC9F005E9818 /* Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63E22C9AC9E005E9818 /* Key.swift */; }; - 6EF1C69622C9AC9F005E9818 /* Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63E22C9AC9E005E9818 /* Key.swift */; }; - 6EF1C69722C9AC9F005E9818 /* Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63E22C9AC9E005E9818 /* Key.swift */; }; - 6EF1C69822C9AC9F005E9818 /* Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63E22C9AC9E005E9818 /* Key.swift */; }; - 6EF1C69922C9AC9F005E9818 /* RemoveKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63F22C9AC9E005E9818 /* RemoveKeyCharacteristic.swift */; }; - 6EF1C69A22C9AC9F005E9818 /* RemoveKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63F22C9AC9E005E9818 /* RemoveKeyCharacteristic.swift */; }; - 6EF1C69B22C9AC9F005E9818 /* RemoveKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63F22C9AC9E005E9818 /* RemoveKeyCharacteristic.swift */; }; - 6EF1C69C22C9AC9F005E9818 /* RemoveKeyCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C63F22C9AC9E005E9818 /* RemoveKeyCharacteristic.swift */; }; - 6EF1C69D22C9AC9F005E9818 /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64022C9AC9E005E9818 /* Crypto.swift */; }; - 6EF1C69E22C9AC9F005E9818 /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64022C9AC9E005E9818 /* Crypto.swift */; }; - 6EF1C69F22C9AC9F005E9818 /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64022C9AC9E005E9818 /* Crypto.swift */; }; - 6EF1C6A022C9AC9F005E9818 /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64022C9AC9E005E9818 /* Crypto.swift */; }; - 6EF1C6A122C9AC9F005E9818 /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64122C9AC9E005E9818 /* UIDevice.swift */; }; - 6EF1C6A222C9AC9F005E9818 /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64122C9AC9E005E9818 /* UIDevice.swift */; }; - 6EF1C6A322C9AC9F005E9818 /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64122C9AC9E005E9818 /* UIDevice.swift */; }; - 6EF1C6A422C9AC9F005E9818 /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64122C9AC9E005E9818 /* UIDevice.swift */; }; - 6EF1C6A522C9AC9F005E9818 /* LockModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64222C9AC9E005E9818 /* LockModel.swift */; }; - 6EF1C6A622C9AC9F005E9818 /* LockModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64222C9AC9E005E9818 /* LockModel.swift */; }; - 6EF1C6A722C9AC9F005E9818 /* LockModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64222C9AC9E005E9818 /* LockModel.swift */; }; - 6EF1C6A822C9AC9F005E9818 /* LockModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64222C9AC9E005E9818 /* LockModel.swift */; }; - 6EF1C6A922C9AC9F005E9818 /* GATTProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64322C9AC9E005E9818 /* GATTProfile.swift */; }; - 6EF1C6AA22C9AC9F005E9818 /* GATTProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64322C9AC9E005E9818 /* GATTProfile.swift */; }; - 6EF1C6AB22C9AC9F005E9818 /* GATTProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64322C9AC9E005E9818 /* GATTProfile.swift */; }; - 6EF1C6AC22C9AC9F005E9818 /* GATTProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64322C9AC9E005E9818 /* GATTProfile.swift */; }; - 6EF1C6AD22C9AC9F005E9818 /* LockState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64422C9AC9E005E9818 /* LockState.swift */; }; - 6EF1C6AE22C9AC9F005E9818 /* LockState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64422C9AC9E005E9818 /* LockState.swift */; }; - 6EF1C6AF22C9AC9F005E9818 /* LockState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64422C9AC9E005E9818 /* LockState.swift */; }; - 6EF1C6B022C9AC9F005E9818 /* LockState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64422C9AC9E005E9818 /* LockState.swift */; }; - 6EF1C6B122C9AC9F005E9818 /* BitMaskOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64522C9AC9E005E9818 /* BitMaskOption.swift */; }; - 6EF1C6B222C9AC9F005E9818 /* BitMaskOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64522C9AC9E005E9818 /* BitMaskOption.swift */; }; - 6EF1C6B322C9AC9F005E9818 /* BitMaskOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64522C9AC9E005E9818 /* BitMaskOption.swift */; }; - 6EF1C6B422C9AC9F005E9818 /* BitMaskOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64522C9AC9E005E9818 /* BitMaskOption.swift */; }; - 6EF1C6B522C9AC9F005E9818 /* GATTError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64622C9AC9E005E9818 /* GATTError.swift */; }; - 6EF1C6B622C9AC9F005E9818 /* GATTError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64622C9AC9E005E9818 /* GATTError.swift */; }; - 6EF1C6B722C9AC9F005E9818 /* GATTError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64622C9AC9E005E9818 /* GATTError.swift */; }; - 6EF1C6B822C9AC9F005E9818 /* GATTError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64622C9AC9E005E9818 /* GATTError.swift */; }; - 6EF1C6B922C9AC9F005E9818 /* UnlockCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64722C9AC9E005E9818 /* UnlockCharacteristic.swift */; }; - 6EF1C6BA22C9AC9F005E9818 /* UnlockCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64722C9AC9E005E9818 /* UnlockCharacteristic.swift */; }; - 6EF1C6BB22C9AC9F005E9818 /* UnlockCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64722C9AC9E005E9818 /* UnlockCharacteristic.swift */; }; - 6EF1C6BC22C9AC9F005E9818 /* UnlockCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64722C9AC9E005E9818 /* UnlockCharacteristic.swift */; }; - 6EF1C6BD22C9AC9F005E9818 /* LockInformationCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64822C9AC9E005E9818 /* LockInformationCharacteristic.swift */; }; - 6EF1C6BE22C9AC9F005E9818 /* LockInformationCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64822C9AC9E005E9818 /* LockInformationCharacteristic.swift */; }; - 6EF1C6BF22C9AC9F005E9818 /* LockInformationCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64822C9AC9E005E9818 /* LockInformationCharacteristic.swift */; }; - 6EF1C6C022C9AC9F005E9818 /* LockInformationCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64822C9AC9E005E9818 /* LockInformationCharacteristic.swift */; }; - 6EF1C6C122C9AC9F005E9818 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64922C9AC9E005E9818 /* Version.swift */; }; - 6EF1C6C222C9AC9F005E9818 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64922C9AC9E005E9818 /* Version.swift */; }; - 6EF1C6C322C9AC9F005E9818 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64922C9AC9E005E9818 /* Version.swift */; }; - 6EF1C6C422C9AC9F005E9818 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64922C9AC9E005E9818 /* Version.swift */; }; - 6EF1C6C522C9AC9F005E9818 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64A22C9AC9E005E9818 /* Status.swift */; }; - 6EF1C6C622C9AC9F005E9818 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64A22C9AC9E005E9818 /* Status.swift */; }; - 6EF1C6C722C9AC9F005E9818 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64A22C9AC9E005E9818 /* Status.swift */; }; - 6EF1C6C822C9AC9F005E9818 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64A22C9AC9E005E9818 /* Status.swift */; }; - 6EF1C6C922C9AC9F005E9818 /* DeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64B22C9AC9E005E9818 /* DeviceManager.swift */; }; - 6EF1C6CA22C9AC9F005E9818 /* DeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64B22C9AC9E005E9818 /* DeviceManager.swift */; }; - 6EF1C6CB22C9AC9F005E9818 /* DeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64B22C9AC9E005E9818 /* DeviceManager.swift */; }; - 6EF1C6CC22C9AC9F005E9818 /* DeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64B22C9AC9E005E9818 /* DeviceManager.swift */; }; - 6EF1C6CD22C9AC9F005E9818 /* TLV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64C22C9AC9F005E9818 /* TLV.swift */; }; - 6EF1C6CE22C9AC9F005E9818 /* TLV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64C22C9AC9F005E9818 /* TLV.swift */; }; - 6EF1C6CF22C9AC9F005E9818 /* TLV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64C22C9AC9F005E9818 /* TLV.swift */; }; - 6EF1C6D022C9AC9F005E9818 /* TLV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64C22C9AC9F005E9818 /* TLV.swift */; }; - 6EF1C6D122C9AC9F005E9818 /* Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64D22C9AC9F005E9818 /* Authentication.swift */; }; - 6EF1C6D222C9AC9F005E9818 /* Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64D22C9AC9F005E9818 /* Authentication.swift */; }; - 6EF1C6D322C9AC9F005E9818 /* Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64D22C9AC9F005E9818 /* Authentication.swift */; }; - 6EF1C6D422C9AC9F005E9818 /* Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64D22C9AC9F005E9818 /* Authentication.swift */; }; - 6EF1C6D522C9AC9F005E9818 /* Permission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64E22C9AC9F005E9818 /* Permission.swift */; }; - 6EF1C6D622C9AC9F005E9818 /* Permission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64E22C9AC9F005E9818 /* Permission.swift */; }; - 6EF1C6D722C9AC9F005E9818 /* Permission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64E22C9AC9F005E9818 /* Permission.swift */; }; - 6EF1C6D822C9AC9F005E9818 /* Permission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64E22C9AC9F005E9818 /* Permission.swift */; }; - 6EF1C6D922C9AC9F005E9818 /* Integer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64F22C9AC9F005E9818 /* Integer.swift */; }; - 6EF1C6DA22C9AC9F005E9818 /* Integer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64F22C9AC9F005E9818 /* Integer.swift */; }; - 6EF1C6DB22C9AC9F005E9818 /* Integer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64F22C9AC9F005E9818 /* Integer.swift */; }; - 6EF1C6DC22C9AC9F005E9818 /* Integer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C64F22C9AC9F005E9818 /* Integer.swift */; }; - 6EF1C6DD22C9AC9F005E9818 /* SmartLockProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65022C9AC9F005E9818 /* SmartLockProfile.swift */; }; - 6EF1C6DE22C9AC9F005E9818 /* SmartLockProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65022C9AC9F005E9818 /* SmartLockProfile.swift */; }; - 6EF1C6DF22C9AC9F005E9818 /* SmartLockProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65022C9AC9F005E9818 /* SmartLockProfile.swift */; }; - 6EF1C6E022C9AC9F005E9818 /* SmartLockProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65022C9AC9F005E9818 /* SmartLockProfile.swift */; }; - 6EF1C6E122C9AC9F005E9818 /* DateComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65122C9AC9F005E9818 /* DateComponents.swift */; }; - 6EF1C6E222C9AC9F005E9818 /* DateComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65122C9AC9F005E9818 /* DateComponents.swift */; }; - 6EF1C6E322C9AC9F005E9818 /* DateComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65122C9AC9F005E9818 /* DateComponents.swift */; }; - 6EF1C6E422C9AC9F005E9818 /* DateComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65122C9AC9F005E9818 /* DateComponents.swift */; }; - 6EF1C6E522C9AC9F005E9818 /* LockHardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65222C9AC9F005E9818 /* LockHardware.swift */; }; - 6EF1C6E622C9AC9F005E9818 /* LockHardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65222C9AC9F005E9818 /* LockHardware.swift */; }; - 6EF1C6E722C9AC9F005E9818 /* LockHardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65222C9AC9F005E9818 /* LockHardware.swift */; }; - 6EF1C6E822C9AC9F005E9818 /* LockHardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65222C9AC9F005E9818 /* LockHardware.swift */; }; - 6EF1C6E922C9AC9F005E9818 /* UnlockAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65322C9AC9F005E9818 /* UnlockAction.swift */; }; - 6EF1C6EA22C9AC9F005E9818 /* UnlockAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65322C9AC9F005E9818 /* UnlockAction.swift */; }; - 6EF1C6EB22C9AC9F005E9818 /* UnlockAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65322C9AC9F005E9818 /* UnlockAction.swift */; }; - 6EF1C6EC22C9AC9F005E9818 /* UnlockAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65322C9AC9F005E9818 /* UnlockAction.swift */; }; - 6EF1C6ED22C9AC9F005E9818 /* Bool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65422C9AC9F005E9818 /* Bool.swift */; }; - 6EF1C6EE22C9AC9F005E9818 /* Bool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65422C9AC9F005E9818 /* Bool.swift */; }; - 6EF1C6EF22C9AC9F005E9818 /* Bool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65422C9AC9F005E9818 /* Bool.swift */; }; - 6EF1C6F022C9AC9F005E9818 /* Bool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C65422C9AC9F005E9818 /* Bool.swift */; }; DD7502881C68FEDE006590AF /* CoreLock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6DA0F1BF000BD002C0205 /* CoreLock.framework */; }; /* End PBXBuildFile section */ @@ -282,69 +282,71 @@ 52D6D9E21BEFFF6E002C0205 /* CoreLock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreLock.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 52D6D9F01BEFFFBE002C0205 /* CoreLock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreLock.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; + 6E01DB3428D683AA004B5956 /* LockAuthorizationStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockAuthorizationStore.swift; sourceTree = ""; }; + 6E01DB3528D683AA004B5956 /* UnlockAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnlockAction.swift; sourceTree = ""; }; + 6E01DB3728D683AA004B5956 /* Advertisement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Advertisement.swift; sourceTree = ""; }; + 6E01DB3828D683AA004B5956 /* ConfirmNewKeyCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfirmNewKeyCharacteristic.swift; sourceTree = ""; }; + 6E01DB3928D683AA004B5956 /* Central.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Central.swift; sourceTree = ""; }; + 6E01DB3A28D683AA004B5956 /* ListEventsCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListEventsCharacteristic.swift; sourceTree = ""; }; + 6E01DB3B28D683AA004B5956 /* UnlockCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnlockCharacteristic.swift; sourceTree = ""; }; + 6E01DB3C28D683AA004B5956 /* SetupCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetupCharacteristic.swift; sourceTree = ""; }; + 6E01DB3D28D683AA004B5956 /* DeviceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceManager.swift; sourceTree = ""; }; + 6E01DB3E28D683AA004B5956 /* LockInformationCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockInformationCharacteristic.swift; sourceTree = ""; }; + 6E01DB3F28D683AA004B5956 /* ListKeysCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListKeysCharacteristic.swift; sourceTree = ""; }; + 6E01DB4028D683AA004B5956 /* KeysCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeysCharacteristic.swift; sourceTree = ""; }; + 6E01DB4128D683AA004B5956 /* CreateNewKeyCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateNewKeyCharacteristic.swift; sourceTree = ""; }; + 6E01DB4228D683AA004B5956 /* TLV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TLV.swift; sourceTree = ""; }; + 6E01DB4328D683AA004B5956 /* Notification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; + 6E01DB4428D683AA004B5956 /* EventsCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventsCharacteristic.swift; sourceTree = ""; }; + 6E01DB4528D683AA004B5956 /* GATTProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GATTProfile.swift; sourceTree = ""; }; + 6E01DB4628D683AA004B5956 /* RemoveKeyCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoveKeyCharacteristic.swift; sourceTree = ""; }; + 6E01DB4728D683AA004B5956 /* GATTError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GATTError.swift; sourceTree = ""; }; + 6E01DB4828D683AA004B5956 /* LockInformation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockInformation.swift; sourceTree = ""; }; + 6E01DB4928D683AA004B5956 /* LockModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockModel.swift; sourceTree = ""; }; + 6E01DB4A28D683AA004B5956 /* BuildVersion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuildVersion.swift; sourceTree = ""; }; + 6E01DB4B28D683AA004B5956 /* KeyCredentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyCredentials.swift; sourceTree = ""; }; + 6E01DB4C28D683AA004B5956 /* POSIXTime.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = POSIXTime.swift; sourceTree = ""; }; + 6E01DB4D28D683AA004B5956 /* EventStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventStore.swift; sourceTree = ""; }; + 6E01DB4E28D683AA004B5956 /* GitCommits.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitCommits.swift; sourceTree = ""; }; + 6E01DB4F28D683AA004B5956 /* Chunk.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Chunk.swift; sourceTree = ""; }; + 6E01DB5028D683AA004B5956 /* Status.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; + 6E01DB5128D683AA004B5956 /* Version.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; + 6E01DB5228D683AA004B5956 /* SmartLockProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SmartLockProfile.swift; sourceTree = ""; }; + 6E01DB5428D683AB004B5956 /* EventsRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventsRequest.swift; sourceTree = ""; }; + 6E01DB5528D683AB004B5956 /* DeleteKeyRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteKeyRequest.swift; sourceTree = ""; }; + 6E01DB5628D683AB004B5956 /* EventsResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventsResponse.swift; sourceTree = ""; }; + 6E01DB5728D683AB004B5956 /* CreateNewKeyRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateNewKeyRequest.swift; sourceTree = ""; }; + 6E01DB5828D683AB004B5956 /* LockNetService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockNetService.swift; sourceTree = ""; }; + 6E01DB5928D683AB004B5956 /* KeysResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeysResponse.swift; sourceTree = ""; }; + 6E01DB5A28D683AB004B5956 /* URLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = ""; }; + 6E01DB5B28D683AB004B5956 /* LockInformationResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockInformationResponse.swift; sourceTree = ""; }; + 6E01DB5C28D683AB004B5956 /* URL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; + 6E01DB5D28D683AB004B5956 /* UpdateRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateRequest.swift; sourceTree = ""; }; + 6E01DB5E28D683AB004B5956 /* KeysRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeysRequest.swift; sourceTree = ""; }; + 6E01DB5F28D683AB004B5956 /* LockInformationRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockInformationRequest.swift; sourceTree = ""; }; + 6E01DB6128D683AB004B5956 /* Authentication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Authentication.swift; sourceTree = ""; }; + 6E01DB6228D683AB004B5956 /* AuthenticationError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationError.swift; sourceTree = ""; }; + 6E01DB6328D683AB004B5956 /* SecureData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureData.swift; sourceTree = ""; }; + 6E01DB6428D683AB004B5956 /* Crypto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = ""; }; + 6E01DB6528D683AB004B5956 /* EncryptedData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncryptedData.swift; sourceTree = ""; }; + 6E01DB6628D683AB004B5956 /* BitMaskOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitMaskOption.swift; sourceTree = ""; }; + 6E01DB6728D683AB004B5956 /* DateComponents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateComponents.swift; sourceTree = ""; }; + 6E01DB6928D683AB004B5956 /* Date.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; + 6E01DB6A28D683AB004B5956 /* UUID.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UUID.swift; sourceTree = ""; }; + 6E01DB6B28D683AB004B5956 /* Integer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Integer.swift; sourceTree = ""; }; + 6E01DB6C28D683AB004B5956 /* Bool.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bool.swift; sourceTree = ""; }; + 6E01DB6D28D683AB004B5956 /* Permission.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Permission.swift; sourceTree = ""; }; + 6E01DB6E28D683AB004B5956 /* LockState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockState.swift; sourceTree = ""; }; + 6E01DB6F28D683AB004B5956 /* NewKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewKey.swift; sourceTree = ""; }; + 6E01DB7028D683AB004B5956 /* LockHardware.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockHardware.swift; sourceTree = ""; }; + 6E01DB7128D683AB004B5956 /* LockConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockConfiguration.swift; sourceTree = ""; }; + 6E01DB7228D683AB004B5956 /* Event.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; + 6E01DB7328D683AB004B5956 /* Key.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Key.swift; sourceTree = ""; }; + 6E01DB7428D683AB004B5956 /* LockConfigurationStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockConfigurationStore.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 = ""; }; - 6EF1C63122C9AC9D005E9818 /* SecureData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureData.swift; sourceTree = ""; }; - 6EF1C63222C9AC9D005E9818 /* AuthenticationError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationError.swift; sourceTree = ""; }; - 6EF1C63322C9AC9D005E9818 /* ListKeysCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListKeysCharacteristic.swift; sourceTree = ""; }; - 6EF1C63422C9AC9D005E9818 /* NewKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewKey.swift; sourceTree = ""; }; - 6EF1C63522C9AC9D005E9818 /* Advertisement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Advertisement.swift; sourceTree = ""; }; - 6EF1C63622C9AC9D005E9818 /* BuildVersion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuildVersion.swift; sourceTree = ""; }; - 6EF1C63722C9AC9D005E9818 /* KeysCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeysCharacteristic.swift; sourceTree = ""; }; - 6EF1C63822C9AC9D005E9818 /* Chunk.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Chunk.swift; sourceTree = ""; }; - 6EF1C63922C9AC9D005E9818 /* Central.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Central.swift; sourceTree = ""; }; - 6EF1C63A22C9AC9E005E9818 /* GitCommits.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitCommits.swift; sourceTree = ""; }; - 6EF1C63B22C9AC9E005E9818 /* POSIXTime.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = POSIXTime.swift; sourceTree = ""; }; - 6EF1C63C22C9AC9E005E9818 /* CreateNewKeyCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateNewKeyCharacteristic.swift; sourceTree = ""; }; - 6EF1C63D22C9AC9E005E9818 /* ConfirmNewKeyCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfirmNewKeyCharacteristic.swift; sourceTree = ""; }; - 6EF1C63E22C9AC9E005E9818 /* Key.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Key.swift; sourceTree = ""; }; - 6EF1C63F22C9AC9E005E9818 /* RemoveKeyCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoveKeyCharacteristic.swift; sourceTree = ""; }; - 6EF1C64022C9AC9E005E9818 /* Crypto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = ""; }; - 6EF1C64122C9AC9E005E9818 /* UIDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIDevice.swift; sourceTree = ""; }; - 6EF1C64222C9AC9E005E9818 /* LockModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockModel.swift; sourceTree = ""; }; - 6EF1C64322C9AC9E005E9818 /* GATTProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GATTProfile.swift; sourceTree = ""; }; - 6EF1C64422C9AC9E005E9818 /* LockState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockState.swift; sourceTree = ""; }; - 6EF1C64522C9AC9E005E9818 /* BitMaskOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitMaskOption.swift; sourceTree = ""; }; - 6EF1C64622C9AC9E005E9818 /* GATTError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GATTError.swift; sourceTree = ""; }; - 6EF1C64722C9AC9E005E9818 /* UnlockCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnlockCharacteristic.swift; sourceTree = ""; }; - 6EF1C64822C9AC9E005E9818 /* LockInformationCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockInformationCharacteristic.swift; sourceTree = ""; }; - 6EF1C64922C9AC9E005E9818 /* Version.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; - 6EF1C64A22C9AC9E005E9818 /* Status.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; - 6EF1C64B22C9AC9E005E9818 /* DeviceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceManager.swift; sourceTree = ""; }; - 6EF1C64C22C9AC9F005E9818 /* TLV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TLV.swift; sourceTree = ""; }; - 6EF1C64D22C9AC9F005E9818 /* Authentication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Authentication.swift; sourceTree = ""; }; - 6EF1C64E22C9AC9F005E9818 /* Permission.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Permission.swift; sourceTree = ""; }; - 6EF1C64F22C9AC9F005E9818 /* Integer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Integer.swift; sourceTree = ""; }; - 6EF1C65022C9AC9F005E9818 /* SmartLockProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SmartLockProfile.swift; sourceTree = ""; }; - 6EF1C65122C9AC9F005E9818 /* DateComponents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateComponents.swift; sourceTree = ""; }; - 6EF1C65222C9AC9F005E9818 /* LockHardware.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockHardware.swift; sourceTree = ""; }; - 6EF1C65322C9AC9F005E9818 /* UnlockAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnlockAction.swift; sourceTree = ""; }; - 6EF1C65422C9AC9F005E9818 /* Bool.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bool.swift; sourceTree = ""; }; AD2FAA261CD0B6D800659CF4 /* CoreLock.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = CoreLock.plist; sourceTree = ""; }; AD2FAA281CD0B6E100659CF4 /* CoreLockTests.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = CoreLockTests.plist; sourceTree = ""; }; DD75027A1C68FCFC006590AF /* CoreLockTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CoreLockTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -355,10 +357,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6EBA70F6235790CB0005BEB7 /* Bonjour in Frameworks */, - 6E621244234721FF007D49BF /* Bluetooth in Frameworks */, 6E9C95ED231149A5007C18FE /* GATT in Frameworks */, - 6E9C95EA23114962007C18FE /* CryptoSwift in Frameworks */, 6E9C95EF231149A5007C18FE /* DarwinGATT in Frameworks */, 6E9C95F2231149F5007C18FE /* TLVCoding in Frameworks */, ); @@ -368,10 +367,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6EBA70FA235790E10005BEB7 /* Bonjour in Frameworks */, - 6E621266234723ED007D49BF /* Bluetooth in Frameworks */, 6ED81FED231662F100B69520 /* GATT in Frameworks */, - 6ED81FE9231662F100B69520 /* CryptoSwift in Frameworks */, 6ED81FEB231662F100B69520 /* DarwinGATT in Frameworks */, 6ED81FEF231662F100B69520 /* TLVCoding in Frameworks */, ); @@ -381,10 +377,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6EBA70FC235790E70005BEB7 /* Bonjour in Frameworks */, - 6E62126A23472400007D49BF /* Bluetooth in Frameworks */, 6ED81FF5231662FB00B69520 /* GATT in Frameworks */, - 6ED81FF1231662FB00B69520 /* CryptoSwift in Frameworks */, 6ED81FF3231662FB00B69520 /* DarwinGATT in Frameworks */, 6ED81FF7231662FB00B69520 /* TLVCoding in Frameworks */, ); @@ -394,10 +387,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6EBA70F8235790D90005BEB7 /* Bonjour in Frameworks */, - 6E62124823472217007D49BF /* Bluetooth in Frameworks */, 6ED81FE5231662E200B69520 /* GATT in Frameworks */, - 6ED81FE1231662E200B69520 /* CryptoSwift in Frameworks */, 6ED81FE3231662E200B69520 /* DarwinGATT in Frameworks */, 6ED81FE7231662E200B69520 /* TLVCoding in Frameworks */, ); @@ -446,11 +436,76 @@ path = Configs; sourceTree = ""; }; + 6E01DB3628D683AA004B5956 /* Bluetooth */ = { + isa = PBXGroup; + children = ( + 6E01DB3728D683AA004B5956 /* Advertisement.swift */, + 6E01DB3828D683AA004B5956 /* ConfirmNewKeyCharacteristic.swift */, + 6E01DB3928D683AA004B5956 /* Central.swift */, + 6E01DB3A28D683AA004B5956 /* ListEventsCharacteristic.swift */, + 6E01DB3B28D683AA004B5956 /* UnlockCharacteristic.swift */, + 6E01DB3C28D683AA004B5956 /* SetupCharacteristic.swift */, + 6E01DB3D28D683AA004B5956 /* DeviceManager.swift */, + 6E01DB3E28D683AA004B5956 /* LockInformationCharacteristic.swift */, + 6E01DB3F28D683AA004B5956 /* ListKeysCharacteristic.swift */, + 6E01DB4028D683AA004B5956 /* KeysCharacteristic.swift */, + 6E01DB4128D683AA004B5956 /* CreateNewKeyCharacteristic.swift */, + 6E01DB4228D683AA004B5956 /* TLV.swift */, + 6E01DB4328D683AA004B5956 /* Notification.swift */, + 6E01DB4428D683AA004B5956 /* EventsCharacteristic.swift */, + 6E01DB4528D683AA004B5956 /* GATTProfile.swift */, + 6E01DB4628D683AA004B5956 /* RemoveKeyCharacteristic.swift */, + 6E01DB4728D683AA004B5956 /* GATTError.swift */, + ); + path = Bluetooth; + sourceTree = ""; + }; + 6E01DB5328D683AB004B5956 /* Networking */ = { + isa = PBXGroup; + children = ( + 6E01DB5428D683AB004B5956 /* EventsRequest.swift */, + 6E01DB5528D683AB004B5956 /* DeleteKeyRequest.swift */, + 6E01DB5628D683AB004B5956 /* EventsResponse.swift */, + 6E01DB5728D683AB004B5956 /* CreateNewKeyRequest.swift */, + 6E01DB5828D683AB004B5956 /* LockNetService.swift */, + 6E01DB5928D683AB004B5956 /* KeysResponse.swift */, + 6E01DB5A28D683AB004B5956 /* URLSession.swift */, + 6E01DB5B28D683AB004B5956 /* LockInformationResponse.swift */, + 6E01DB5C28D683AB004B5956 /* URL.swift */, + 6E01DB5D28D683AB004B5956 /* UpdateRequest.swift */, + 6E01DB5E28D683AB004B5956 /* KeysRequest.swift */, + 6E01DB5F28D683AB004B5956 /* LockInformationRequest.swift */, + ); + path = Networking; + sourceTree = ""; + }; + 6E01DB6028D683AB004B5956 /* Crypto */ = { + isa = PBXGroup; + children = ( + 6E01DB6128D683AB004B5956 /* Authentication.swift */, + 6E01DB6228D683AB004B5956 /* AuthenticationError.swift */, + 6E01DB6328D683AB004B5956 /* SecureData.swift */, + 6E01DB6428D683AB004B5956 /* Crypto.swift */, + 6E01DB6528D683AB004B5956 /* EncryptedData.swift */, + ); + path = Crypto; + sourceTree = ""; + }; + 6E01DB6828D683AB004B5956 /* Extensions */ = { + isa = PBXGroup; + children = ( + 6E01DB6928D683AB004B5956 /* Date.swift */, + 6E01DB6A28D683AB004B5956 /* UUID.swift */, + 6E01DB6B28D683AB004B5956 /* Integer.swift */, + 6E01DB6C28D683AB004B5956 /* Bool.swift */, + ); + path = Extensions; + sourceTree = ""; + }; 6E60FF512121041400787DAA /* Tests */ = { isa = PBXGroup; children = ( 6E60FF522121041400787DAA /* CoreLockTests */, - 6E60FF552121041400787DAA /* LinuxMain.swift */, 6E60FF562121041400787DAA /* CoreLockGATTServerTests */, ); name = Tests; @@ -487,64 +542,33 @@ 6E60FF612121041400787DAA /* CoreLock */ = { isa = PBXGroup; children = ( - 6EF1C63522C9AC9D005E9818 /* Advertisement.swift */, - 6EF1C64D22C9AC9F005E9818 /* Authentication.swift */, - 6EF1C63222C9AC9D005E9818 /* AuthenticationError.swift */, - 6EF1C64522C9AC9E005E9818 /* BitMaskOption.swift */, - 6EF1C65422C9AC9F005E9818 /* Bool.swift */, - 6EF1C63622C9AC9D005E9818 /* BuildVersion.swift */, - 6EF1C63922C9AC9D005E9818 /* Central.swift */, - 6EF1C63822C9AC9D005E9818 /* Chunk.swift */, - 6EF1C63D22C9AC9E005E9818 /* ConfirmNewKeyCharacteristic.swift */, - 6EF1C63C22C9AC9E005E9818 /* CreateNewKeyCharacteristic.swift */, - 6EF1C64022C9AC9E005E9818 /* Crypto.swift */, - 6EF1C65122C9AC9F005E9818 /* DateComponents.swift */, - 6EF1C64B22C9AC9E005E9818 /* DeviceManager.swift */, - 6EBA70FD2357A7040005BEB7 /* URLSession.swift */, - 6EF1C63022C9AC9D005E9818 /* EncryptedData.swift */, - 6EF1C64622C9AC9E005E9818 /* GATTError.swift */, - 6EF1C64322C9AC9E005E9818 /* GATTProfile.swift */, - 6EF1C63A22C9AC9E005E9818 /* GitCommits.swift */, - 6EF1C64F22C9AC9F005E9818 /* Integer.swift */, - 6EF1C63E22C9AC9E005E9818 /* Key.swift */, - 6EF1C63722C9AC9D005E9818 /* KeysCharacteristic.swift */, - 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 */, - 6EF1C63B22C9AC9E005E9818 /* POSIXTime.swift */, - 6EF1C63F22C9AC9E005E9818 /* RemoveKeyCharacteristic.swift */, - 6EF1C63122C9AC9D005E9818 /* SecureData.swift */, - 6EF1C62E22C9AC9D005E9818 /* SetupCharacteristic.swift */, - 6EF1C65022C9AC9F005E9818 /* SmartLockProfile.swift */, - 6EF1C64A22C9AC9E005E9818 /* Status.swift */, - 6EF1C64C22C9AC9F005E9818 /* TLV.swift */, - 6EF1C64122C9AC9E005E9818 /* UIDevice.swift */, - 6EF1C65322C9AC9F005E9818 /* UnlockAction.swift */, - 6EF1C64722C9AC9E005E9818 /* UnlockCharacteristic.swift */, - 6EF1C64922C9AC9E005E9818 /* Version.swift */, - 6EACDA98231B3539000CF82A /* Event.swift */, - 6EE1D79E235801BF004DD856 /* EventStore.swift */, - 6E0A32D422D8FCFB002EF9DE /* URL.swift */, - 6EE1D6EE2357FAF2004DD856 /* URLSession.swift */, - 6E9794C323301FC400B5C5C9 /* UUID.swift */, - 6E4729EA235AE7B3007CBC07 /* UpdateRequest.swift */, - 6E4729FD235B768E007CBC07 /* DeleteKeyRequest.swift */, + 6E01DB6628D683AB004B5956 /* BitMaskOption.swift */, + 6E01DB3628D683AA004B5956 /* Bluetooth */, + 6E01DB4A28D683AA004B5956 /* BuildVersion.swift */, + 6E01DB4F28D683AA004B5956 /* Chunk.swift */, + 6E01DB6028D683AB004B5956 /* Crypto */, + 6E01DB6728D683AB004B5956 /* DateComponents.swift */, + 6E01DB7228D683AB004B5956 /* Event.swift */, + 6E01DB4D28D683AA004B5956 /* EventStore.swift */, + 6E01DB6828D683AB004B5956 /* Extensions */, + 6E01DB4E28D683AA004B5956 /* GitCommits.swift */, + 6E01DB7328D683AB004B5956 /* Key.swift */, + 6E01DB4B28D683AA004B5956 /* KeyCredentials.swift */, + 6E01DB3428D683AA004B5956 /* LockAuthorizationStore.swift */, + 6E01DB7128D683AB004B5956 /* LockConfiguration.swift */, + 6E01DB7428D683AB004B5956 /* LockConfigurationStore.swift */, + 6E01DB7028D683AB004B5956 /* LockHardware.swift */, + 6E01DB4828D683AA004B5956 /* LockInformation.swift */, + 6E01DB4928D683AA004B5956 /* LockModel.swift */, + 6E01DB6E28D683AB004B5956 /* LockState.swift */, + 6E01DB5328D683AB004B5956 /* Networking */, + 6E01DB6F28D683AB004B5956 /* NewKey.swift */, + 6E01DB6D28D683AB004B5956 /* Permission.swift */, + 6E01DB4C28D683AA004B5956 /* POSIXTime.swift */, + 6E01DB5228D683AA004B5956 /* SmartLockProfile.swift */, + 6E01DB5028D683AA004B5956 /* Status.swift */, + 6E01DB3528D683AA004B5956 /* UnlockAction.swift */, + 6E01DB5128D683AA004B5956 /* Version.swift */, ); path = CoreLock; sourceTree = ""; @@ -621,12 +645,9 @@ ); name = "CoreLock-iOS"; packageProductDependencies = ( - 6E9C95E923114962007C18FE /* CryptoSwift */, 6E9C95EC231149A5007C18FE /* GATT */, 6E9C95EE231149A5007C18FE /* DarwinGATT */, 6E9C95F1231149F5007C18FE /* TLVCoding */, - 6E621243234721FF007D49BF /* Bluetooth */, - 6EBA70F5235790CB0005BEB7 /* Bonjour */, ); productName = SmartLock; productReference = 52D6D97C1BEFF229002C0205 /* CoreLock.framework */; @@ -647,12 +668,9 @@ ); name = "CoreLock-watchOS"; packageProductDependencies = ( - 6ED81FE8231662F100B69520 /* CryptoSwift */, 6ED81FEA231662F100B69520 /* DarwinGATT */, 6ED81FEC231662F100B69520 /* GATT */, 6ED81FEE231662F100B69520 /* TLVCoding */, - 6E621265234723ED007D49BF /* Bluetooth */, - 6EBA70F9235790E10005BEB7 /* Bonjour */, ); productName = "SmartLock-watchOS"; productReference = 52D6D9E21BEFFF6E002C0205 /* CoreLock.framework */; @@ -673,12 +691,9 @@ ); name = "CoreLock-tvOS"; packageProductDependencies = ( - 6ED81FF0231662FB00B69520 /* CryptoSwift */, 6ED81FF2231662FB00B69520 /* DarwinGATT */, 6ED81FF4231662FB00B69520 /* GATT */, 6ED81FF6231662FB00B69520 /* TLVCoding */, - 6E62126923472400007D49BF /* Bluetooth */, - 6EBA70FB235790E70005BEB7 /* Bonjour */, ); productName = "SmartLock-tvOS"; productReference = 52D6D9F01BEFFFBE002C0205 /* CoreLock.framework */; @@ -699,12 +714,9 @@ ); name = "CoreLock-macOS"; packageProductDependencies = ( - 6ED81FE0231662E200B69520 /* CryptoSwift */, 6ED81FE2231662E200B69520 /* DarwinGATT */, 6ED81FE4231662E200B69520 /* GATT */, 6ED81FE6231662E200B69520 /* TLVCoding */, - 6E62124723472217007D49BF /* Bluetooth */, - 6EBA70F7235790D90005BEB7 /* Bonjour */, ); productName = "SmartLock-macOS"; productReference = 52D6DA0F1BF000BD002C0205 /* CoreLock.framework */; @@ -740,19 +752,19 @@ TargetAttributes = { 52D6D97B1BEFF229002C0205 = { CreatedOnToolsVersion = 7.1; - LastSwiftMigration = 1020; + LastSwiftMigration = 1400; }; 52D6D9E11BEFFF6E002C0205 = { CreatedOnToolsVersion = 7.1; - LastSwiftMigration = 1020; + LastSwiftMigration = 1400; }; 52D6D9EF1BEFFFBE002C0205 = { CreatedOnToolsVersion = 7.1; - LastSwiftMigration = 1020; + LastSwiftMigration = 1400; }; 52D6DA0E1BF000BD002C0205 = { CreatedOnToolsVersion = 7.1; - LastSwiftMigration = 1020; + LastSwiftMigration = 1400; }; DD7502791C68FCFC006590AF = { CreatedOnToolsVersion = 7.2.1; @@ -770,11 +782,8 @@ ); mainGroup = 52D6D9721BEFF229002C0205; packageReferences = ( - 6E9C95E823114962007C18FE /* XCRemoteSwiftPackageReference "CryptoSwift" */, 6E9C95EB231149A5007C18FE /* XCRemoteSwiftPackageReference "GATT" */, 6E9C95F0231149F5007C18FE /* XCRemoteSwiftPackageReference "TLVCoding" */, - 6E621242234721FF007D49BF /* XCRemoteSwiftPackageReference "Bluetooth" */, - 6EBA70F4235790CB0005BEB7 /* XCRemoteSwiftPackageReference "Bonjour" */, ); productRefGroup = 52D6D97D1BEFF229002C0205 /* Products */; projectDirPath = ""; @@ -832,64 +841,67 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 6EF1C68D22C9AC9F005E9818 /* CreateNewKeyCharacteristic.swift in Sources */, - 6EF1C67522C9AC9F005E9818 /* BuildVersion.swift in Sources */, - 6EF1C6ED22C9AC9F005E9818 /* Bool.swift in Sources */, - 6EF1C6B122C9AC9F005E9818 /* BitMaskOption.swift in Sources */, - 6EF1C6E122C9AC9F005E9818 /* DateComponents.swift in Sources */, - 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 */, - 6EF1C65522C9AC9F005E9818 /* SetupCharacteristic.swift in Sources */, - 6EF1C6A122C9AC9F005E9818 /* UIDevice.swift in Sources */, - 6EF1C67D22C9AC9F005E9818 /* Chunk.swift in Sources */, - 6E9794C423301FC400B5C5C9 /* UUID.swift in Sources */, - 6EF1C6E922C9AC9F005E9818 /* UnlockAction.swift in Sources */, - 6EF1C6AD22C9AC9F005E9818 /* LockState.swift in Sources */, - 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 */, + 6E01DBA128D683AB004B5956 /* KeysCharacteristic.swift in Sources */, + 6E01DBD928D683AB004B5956 /* GitCommits.swift in Sources */, + 6E01DC3128D683AB004B5956 /* BitMaskOption.swift in Sources */, + 6E01DB7528D683AB004B5956 /* LockAuthorizationStore.swift in Sources */, + 6E01DBB928D683AB004B5956 /* RemoveKeyCharacteristic.swift in Sources */, + 6E01DBE928D683AB004B5956 /* SmartLockProfile.swift in Sources */, + 6E01DC0D28D683AB004B5956 /* URL.swift in Sources */, + 6E01DC1D28D683AB004B5956 /* Authentication.swift in Sources */, + 6E01DBC528D683AB004B5956 /* LockModel.swift in Sources */, + 6E01DC4D28D683AB004B5956 /* LockState.swift in Sources */, + 6E01DC5D28D683AB004B5956 /* Event.swift in Sources */, + 6E01DBF528D683AB004B5956 /* EventsResponse.swift in Sources */, + 6E01DBBD28D683AB004B5956 /* GATTError.swift in Sources */, + 6E01DC2128D683AB004B5956 /* AuthenticationError.swift in Sources */, + 6E01DC0128D683AB004B5956 /* KeysResponse.swift in Sources */, + 6E01DC1128D683AB004B5956 /* UpdateRequest.swift in Sources */, + 6E01DC4928D683AB004B5956 /* Permission.swift in Sources */, + 6E01DBC928D683AB004B5956 /* BuildVersion.swift in Sources */, + 6E01DB7928D683AB004B5956 /* UnlockAction.swift in Sources */, + 6E01DBDD28D683AB004B5956 /* Chunk.swift in Sources */, + 6E01DC1528D683AB004B5956 /* KeysRequest.swift in Sources */, + 6E01DC0928D683AB004B5956 /* LockInformationResponse.swift in Sources */, + 6E01DBB128D683AB004B5956 /* EventsCharacteristic.swift in Sources */, + 6E01DBED28D683AB004B5956 /* EventsRequest.swift in Sources */, + 6E01DC5928D683AB004B5956 /* LockConfiguration.swift in Sources */, + 6E01DBA528D683AB004B5956 /* CreateNewKeyCharacteristic.swift in Sources */, + 6E01DBA928D683AB004B5956 /* TLV.swift in Sources */, + 6E01DBD528D683AB004B5956 /* EventStore.swift in Sources */, + 6E01DC3528D683AB004B5956 /* DateComponents.swift in Sources */, + 6E01DBB528D683AB004B5956 /* GATTProfile.swift in Sources */, + 6E01DC4128D683AB004B5956 /* Integer.swift in Sources */, + 6E01DC5528D683AB004B5956 /* LockHardware.swift in Sources */, + 6E01DC6528D683AB004B5956 /* LockConfigurationStore.swift in Sources */, + 6E01DBFD28D683AB004B5956 /* LockNetService.swift in Sources */, + 6E01DB8528D683AB004B5956 /* Central.swift in Sources */, + 6E01DB9D28D683AB004B5956 /* ListKeysCharacteristic.swift in Sources */, + 6E01DBE528D683AB004B5956 /* Version.swift in Sources */, + 6E01DC3D28D683AB004B5956 /* UUID.swift in Sources */, + 6E01DC2D28D683AB004B5956 /* EncryptedData.swift in Sources */, + 6E01DC3928D683AB004B5956 /* Date.swift in Sources */, + 6E01DB8928D683AB004B5956 /* ListEventsCharacteristic.swift in Sources */, + 6E01DB8D28D683AB004B5956 /* UnlockCharacteristic.swift in Sources */, + 6E01DC6128D683AB004B5956 /* Key.swift in Sources */, + 6E01DC0528D683AB004B5956 /* URLSession.swift in Sources */, + 6E01DB8128D683AB004B5956 /* ConfirmNewKeyCharacteristic.swift in Sources */, + 6E01DBF128D683AB004B5956 /* DeleteKeyRequest.swift in Sources */, + 6E01DB9928D683AB004B5956 /* LockInformationCharacteristic.swift in Sources */, + 6E01DBCD28D683AB004B5956 /* KeyCredentials.swift in Sources */, + 6E01DBC128D683AB004B5956 /* LockInformation.swift in Sources */, + 6E01DC2928D683AB004B5956 /* Crypto.swift in Sources */, + 6E01DBAD28D683AB004B5956 /* Notification.swift in Sources */, + 6E01DC4528D683AB004B5956 /* Bool.swift in Sources */, + 6E01DBF928D683AB004B5956 /* CreateNewKeyRequest.swift in Sources */, + 6E01DBE128D683AB004B5956 /* Status.swift in Sources */, + 6E01DBD128D683AB004B5956 /* POSIXTime.swift in Sources */, + 6E01DC5128D683AB004B5956 /* NewKey.swift in Sources */, + 6E01DB9128D683AB004B5956 /* SetupCharacteristic.swift in Sources */, + 6E01DC1928D683AB004B5956 /* LockInformationRequest.swift in Sources */, + 6E01DB9528D683AB004B5956 /* DeviceManager.swift in Sources */, + 6E01DC2528D683AB004B5956 /* SecureData.swift in Sources */, + 6E01DB7D28D683AB004B5956 /* Advertisement.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -897,64 +909,67 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 6EF1C68F22C9AC9F005E9818 /* CreateNewKeyCharacteristic.swift in Sources */, - 6EF1C67722C9AC9F005E9818 /* BuildVersion.swift in Sources */, - 6EF1C6EF22C9AC9F005E9818 /* Bool.swift in Sources */, - 6EF1C6B322C9AC9F005E9818 /* BitMaskOption.swift in Sources */, - 6EF1C6E322C9AC9F005E9818 /* DateComponents.swift in Sources */, - 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 */, - 6EF1C65722C9AC9F005E9818 /* SetupCharacteristic.swift in Sources */, - 6EF1C6A322C9AC9F005E9818 /* UIDevice.swift in Sources */, - 6EF1C67F22C9AC9F005E9818 /* Chunk.swift in Sources */, - 6E9794C623301FC400B5C5C9 /* UUID.swift in Sources */, - 6EF1C6EB22C9AC9F005E9818 /* UnlockAction.swift in Sources */, - 6EF1C6AF22C9AC9F005E9818 /* LockState.swift in Sources */, - 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 */, + 6E01DBA328D683AB004B5956 /* KeysCharacteristic.swift in Sources */, + 6E01DBDB28D683AB004B5956 /* GitCommits.swift in Sources */, + 6E01DC3328D683AB004B5956 /* BitMaskOption.swift in Sources */, + 6E01DB7728D683AB004B5956 /* LockAuthorizationStore.swift in Sources */, + 6E01DBBB28D683AB004B5956 /* RemoveKeyCharacteristic.swift in Sources */, + 6E01DBEB28D683AB004B5956 /* SmartLockProfile.swift in Sources */, + 6E01DC0F28D683AB004B5956 /* URL.swift in Sources */, + 6E01DC1F28D683AB004B5956 /* Authentication.swift in Sources */, + 6E01DBC728D683AB004B5956 /* LockModel.swift in Sources */, + 6E01DC4F28D683AB004B5956 /* LockState.swift in Sources */, + 6E01DC5F28D683AB004B5956 /* Event.swift in Sources */, + 6E01DBF728D683AB004B5956 /* EventsResponse.swift in Sources */, + 6E01DBBF28D683AB004B5956 /* GATTError.swift in Sources */, + 6E01DC2328D683AB004B5956 /* AuthenticationError.swift in Sources */, + 6E01DC0328D683AB004B5956 /* KeysResponse.swift in Sources */, + 6E01DC1328D683AB004B5956 /* UpdateRequest.swift in Sources */, + 6E01DC4B28D683AB004B5956 /* Permission.swift in Sources */, + 6E01DBCB28D683AB004B5956 /* BuildVersion.swift in Sources */, + 6E01DB7B28D683AB004B5956 /* UnlockAction.swift in Sources */, + 6E01DBDF28D683AB004B5956 /* Chunk.swift in Sources */, + 6E01DC1728D683AB004B5956 /* KeysRequest.swift in Sources */, + 6E01DC0B28D683AB004B5956 /* LockInformationResponse.swift in Sources */, + 6E01DBB328D683AB004B5956 /* EventsCharacteristic.swift in Sources */, + 6E01DBEF28D683AB004B5956 /* EventsRequest.swift in Sources */, + 6E01DC5B28D683AB004B5956 /* LockConfiguration.swift in Sources */, + 6E01DBA728D683AB004B5956 /* CreateNewKeyCharacteristic.swift in Sources */, + 6E01DBAB28D683AB004B5956 /* TLV.swift in Sources */, + 6E01DBD728D683AB004B5956 /* EventStore.swift in Sources */, + 6E01DC3728D683AB004B5956 /* DateComponents.swift in Sources */, + 6E01DBB728D683AB004B5956 /* GATTProfile.swift in Sources */, + 6E01DC4328D683AB004B5956 /* Integer.swift in Sources */, + 6E01DC5728D683AB004B5956 /* LockHardware.swift in Sources */, + 6E01DC6728D683AB004B5956 /* LockConfigurationStore.swift in Sources */, + 6E01DBFF28D683AB004B5956 /* LockNetService.swift in Sources */, + 6E01DB8728D683AB004B5956 /* Central.swift in Sources */, + 6E01DB9F28D683AB004B5956 /* ListKeysCharacteristic.swift in Sources */, + 6E01DBE728D683AB004B5956 /* Version.swift in Sources */, + 6E01DC3F28D683AB004B5956 /* UUID.swift in Sources */, + 6E01DC2F28D683AB004B5956 /* EncryptedData.swift in Sources */, + 6E01DC3B28D683AB004B5956 /* Date.swift in Sources */, + 6E01DB8B28D683AB004B5956 /* ListEventsCharacteristic.swift in Sources */, + 6E01DB8F28D683AB004B5956 /* UnlockCharacteristic.swift in Sources */, + 6E01DC6328D683AB004B5956 /* Key.swift in Sources */, + 6E01DC0728D683AB004B5956 /* URLSession.swift in Sources */, + 6E01DB8328D683AB004B5956 /* ConfirmNewKeyCharacteristic.swift in Sources */, + 6E01DBF328D683AB004B5956 /* DeleteKeyRequest.swift in Sources */, + 6E01DB9B28D683AB004B5956 /* LockInformationCharacteristic.swift in Sources */, + 6E01DBCF28D683AB004B5956 /* KeyCredentials.swift in Sources */, + 6E01DBC328D683AB004B5956 /* LockInformation.swift in Sources */, + 6E01DC2B28D683AB004B5956 /* Crypto.swift in Sources */, + 6E01DBAF28D683AB004B5956 /* Notification.swift in Sources */, + 6E01DC4728D683AB004B5956 /* Bool.swift in Sources */, + 6E01DBFB28D683AB004B5956 /* CreateNewKeyRequest.swift in Sources */, + 6E01DBE328D683AB004B5956 /* Status.swift in Sources */, + 6E01DBD328D683AB004B5956 /* POSIXTime.swift in Sources */, + 6E01DC5328D683AB004B5956 /* NewKey.swift in Sources */, + 6E01DB9328D683AB004B5956 /* SetupCharacteristic.swift in Sources */, + 6E01DC1B28D683AB004B5956 /* LockInformationRequest.swift in Sources */, + 6E01DB9728D683AB004B5956 /* DeviceManager.swift in Sources */, + 6E01DC2728D683AB004B5956 /* SecureData.swift in Sources */, + 6E01DB7F28D683AB004B5956 /* Advertisement.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -962,64 +977,67 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 6EF1C69022C9AC9F005E9818 /* CreateNewKeyCharacteristic.swift in Sources */, - 6EF1C67822C9AC9F005E9818 /* BuildVersion.swift in Sources */, - 6EF1C6F022C9AC9F005E9818 /* Bool.swift in Sources */, - 6EF1C6B422C9AC9F005E9818 /* BitMaskOption.swift in Sources */, - 6EF1C6E422C9AC9F005E9818 /* DateComponents.swift in Sources */, - 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 */, - 6EF1C65822C9AC9F005E9818 /* SetupCharacteristic.swift in Sources */, - 6EF1C6A422C9AC9F005E9818 /* UIDevice.swift in Sources */, - 6EF1C68022C9AC9F005E9818 /* Chunk.swift in Sources */, - 6E9794C723301FC400B5C5C9 /* UUID.swift in Sources */, - 6EF1C6EC22C9AC9F005E9818 /* UnlockAction.swift in Sources */, - 6EF1C6B022C9AC9F005E9818 /* LockState.swift in Sources */, - 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 */, + 6E01DBA428D683AB004B5956 /* KeysCharacteristic.swift in Sources */, + 6E01DBDC28D683AB004B5956 /* GitCommits.swift in Sources */, + 6E01DC3428D683AB004B5956 /* BitMaskOption.swift in Sources */, + 6E01DB7828D683AB004B5956 /* LockAuthorizationStore.swift in Sources */, + 6E01DBBC28D683AB004B5956 /* RemoveKeyCharacteristic.swift in Sources */, + 6E01DBEC28D683AB004B5956 /* SmartLockProfile.swift in Sources */, + 6E01DC1028D683AB004B5956 /* URL.swift in Sources */, + 6E01DC2028D683AB004B5956 /* Authentication.swift in Sources */, + 6E01DBC828D683AB004B5956 /* LockModel.swift in Sources */, + 6E01DC5028D683AB004B5956 /* LockState.swift in Sources */, + 6E01DC6028D683AB004B5956 /* Event.swift in Sources */, + 6E01DBF828D683AB004B5956 /* EventsResponse.swift in Sources */, + 6E01DBC028D683AB004B5956 /* GATTError.swift in Sources */, + 6E01DC2428D683AB004B5956 /* AuthenticationError.swift in Sources */, + 6E01DC0428D683AB004B5956 /* KeysResponse.swift in Sources */, + 6E01DC1428D683AB004B5956 /* UpdateRequest.swift in Sources */, + 6E01DC4C28D683AB004B5956 /* Permission.swift in Sources */, + 6E01DBCC28D683AB004B5956 /* BuildVersion.swift in Sources */, + 6E01DB7C28D683AB004B5956 /* UnlockAction.swift in Sources */, + 6E01DBE028D683AB004B5956 /* Chunk.swift in Sources */, + 6E01DC1828D683AB004B5956 /* KeysRequest.swift in Sources */, + 6E01DC0C28D683AB004B5956 /* LockInformationResponse.swift in Sources */, + 6E01DBB428D683AB004B5956 /* EventsCharacteristic.swift in Sources */, + 6E01DBF028D683AB004B5956 /* EventsRequest.swift in Sources */, + 6E01DC5C28D683AB004B5956 /* LockConfiguration.swift in Sources */, + 6E01DBA828D683AB004B5956 /* CreateNewKeyCharacteristic.swift in Sources */, + 6E01DBAC28D683AB004B5956 /* TLV.swift in Sources */, + 6E01DBD828D683AB004B5956 /* EventStore.swift in Sources */, + 6E01DC3828D683AB004B5956 /* DateComponents.swift in Sources */, + 6E01DBB828D683AB004B5956 /* GATTProfile.swift in Sources */, + 6E01DC4428D683AB004B5956 /* Integer.swift in Sources */, + 6E01DC5828D683AB004B5956 /* LockHardware.swift in Sources */, + 6E01DC6828D683AB004B5956 /* LockConfigurationStore.swift in Sources */, + 6E01DC0028D683AB004B5956 /* LockNetService.swift in Sources */, + 6E01DB8828D683AB004B5956 /* Central.swift in Sources */, + 6E01DBA028D683AB004B5956 /* ListKeysCharacteristic.swift in Sources */, + 6E01DBE828D683AB004B5956 /* Version.swift in Sources */, + 6E01DC4028D683AB004B5956 /* UUID.swift in Sources */, + 6E01DC3028D683AB004B5956 /* EncryptedData.swift in Sources */, + 6E01DC3C28D683AB004B5956 /* Date.swift in Sources */, + 6E01DB8C28D683AB004B5956 /* ListEventsCharacteristic.swift in Sources */, + 6E01DB9028D683AB004B5956 /* UnlockCharacteristic.swift in Sources */, + 6E01DC6428D683AB004B5956 /* Key.swift in Sources */, + 6E01DC0828D683AB004B5956 /* URLSession.swift in Sources */, + 6E01DB8428D683AB004B5956 /* ConfirmNewKeyCharacteristic.swift in Sources */, + 6E01DBF428D683AB004B5956 /* DeleteKeyRequest.swift in Sources */, + 6E01DB9C28D683AB004B5956 /* LockInformationCharacteristic.swift in Sources */, + 6E01DBD028D683AB004B5956 /* KeyCredentials.swift in Sources */, + 6E01DBC428D683AB004B5956 /* LockInformation.swift in Sources */, + 6E01DC2C28D683AB004B5956 /* Crypto.swift in Sources */, + 6E01DBB028D683AB004B5956 /* Notification.swift in Sources */, + 6E01DC4828D683AB004B5956 /* Bool.swift in Sources */, + 6E01DBFC28D683AB004B5956 /* CreateNewKeyRequest.swift in Sources */, + 6E01DBE428D683AB004B5956 /* Status.swift in Sources */, + 6E01DBD428D683AB004B5956 /* POSIXTime.swift in Sources */, + 6E01DC5428D683AB004B5956 /* NewKey.swift in Sources */, + 6E01DB9428D683AB004B5956 /* SetupCharacteristic.swift in Sources */, + 6E01DC1C28D683AB004B5956 /* LockInformationRequest.swift in Sources */, + 6E01DB9828D683AB004B5956 /* DeviceManager.swift in Sources */, + 6E01DC2828D683AB004B5956 /* SecureData.swift in Sources */, + 6E01DB8028D683AB004B5956 /* Advertisement.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1027,64 +1045,67 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 6EF1C68E22C9AC9F005E9818 /* CreateNewKeyCharacteristic.swift in Sources */, - 6EF1C67622C9AC9F005E9818 /* BuildVersion.swift in Sources */, - 6EF1C6EE22C9AC9F005E9818 /* Bool.swift in Sources */, - 6EF1C6B222C9AC9F005E9818 /* BitMaskOption.swift in Sources */, - 6EF1C6E222C9AC9F005E9818 /* DateComponents.swift in Sources */, - 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 */, - 6EF1C65622C9AC9F005E9818 /* SetupCharacteristic.swift in Sources */, - 6EF1C6A222C9AC9F005E9818 /* UIDevice.swift in Sources */, - 6EF1C67E22C9AC9F005E9818 /* Chunk.swift in Sources */, - 6E9794C523301FC400B5C5C9 /* UUID.swift in Sources */, - 6EF1C6EA22C9AC9F005E9818 /* UnlockAction.swift in Sources */, - 6EF1C6AE22C9AC9F005E9818 /* LockState.swift in Sources */, - 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 */, + 6E01DBA228D683AB004B5956 /* KeysCharacteristic.swift in Sources */, + 6E01DBDA28D683AB004B5956 /* GitCommits.swift in Sources */, + 6E01DC3228D683AB004B5956 /* BitMaskOption.swift in Sources */, + 6E01DB7628D683AB004B5956 /* LockAuthorizationStore.swift in Sources */, + 6E01DBBA28D683AB004B5956 /* RemoveKeyCharacteristic.swift in Sources */, + 6E01DBEA28D683AB004B5956 /* SmartLockProfile.swift in Sources */, + 6E01DC0E28D683AB004B5956 /* URL.swift in Sources */, + 6E01DC1E28D683AB004B5956 /* Authentication.swift in Sources */, + 6E01DBC628D683AB004B5956 /* LockModel.swift in Sources */, + 6E01DC4E28D683AB004B5956 /* LockState.swift in Sources */, + 6E01DC5E28D683AB004B5956 /* Event.swift in Sources */, + 6E01DBF628D683AB004B5956 /* EventsResponse.swift in Sources */, + 6E01DBBE28D683AB004B5956 /* GATTError.swift in Sources */, + 6E01DC2228D683AB004B5956 /* AuthenticationError.swift in Sources */, + 6E01DC0228D683AB004B5956 /* KeysResponse.swift in Sources */, + 6E01DC1228D683AB004B5956 /* UpdateRequest.swift in Sources */, + 6E01DC4A28D683AB004B5956 /* Permission.swift in Sources */, + 6E01DBCA28D683AB004B5956 /* BuildVersion.swift in Sources */, + 6E01DB7A28D683AB004B5956 /* UnlockAction.swift in Sources */, + 6E01DBDE28D683AB004B5956 /* Chunk.swift in Sources */, + 6E01DC1628D683AB004B5956 /* KeysRequest.swift in Sources */, + 6E01DC0A28D683AB004B5956 /* LockInformationResponse.swift in Sources */, + 6E01DBB228D683AB004B5956 /* EventsCharacteristic.swift in Sources */, + 6E01DBEE28D683AB004B5956 /* EventsRequest.swift in Sources */, + 6E01DC5A28D683AB004B5956 /* LockConfiguration.swift in Sources */, + 6E01DBA628D683AB004B5956 /* CreateNewKeyCharacteristic.swift in Sources */, + 6E01DBAA28D683AB004B5956 /* TLV.swift in Sources */, + 6E01DBD628D683AB004B5956 /* EventStore.swift in Sources */, + 6E01DC3628D683AB004B5956 /* DateComponents.swift in Sources */, + 6E01DBB628D683AB004B5956 /* GATTProfile.swift in Sources */, + 6E01DC4228D683AB004B5956 /* Integer.swift in Sources */, + 6E01DC5628D683AB004B5956 /* LockHardware.swift in Sources */, + 6E01DC6628D683AB004B5956 /* LockConfigurationStore.swift in Sources */, + 6E01DBFE28D683AB004B5956 /* LockNetService.swift in Sources */, + 6E01DB8628D683AB004B5956 /* Central.swift in Sources */, + 6E01DB9E28D683AB004B5956 /* ListKeysCharacteristic.swift in Sources */, + 6E01DBE628D683AB004B5956 /* Version.swift in Sources */, + 6E01DC3E28D683AB004B5956 /* UUID.swift in Sources */, + 6E01DC2E28D683AB004B5956 /* EncryptedData.swift in Sources */, + 6E01DC3A28D683AB004B5956 /* Date.swift in Sources */, + 6E01DB8A28D683AB004B5956 /* ListEventsCharacteristic.swift in Sources */, + 6E01DB8E28D683AB004B5956 /* UnlockCharacteristic.swift in Sources */, + 6E01DC6228D683AB004B5956 /* Key.swift in Sources */, + 6E01DC0628D683AB004B5956 /* URLSession.swift in Sources */, + 6E01DB8228D683AB004B5956 /* ConfirmNewKeyCharacteristic.swift in Sources */, + 6E01DBF228D683AB004B5956 /* DeleteKeyRequest.swift in Sources */, + 6E01DB9A28D683AB004B5956 /* LockInformationCharacteristic.swift in Sources */, + 6E01DBCE28D683AB004B5956 /* KeyCredentials.swift in Sources */, + 6E01DBC228D683AB004B5956 /* LockInformation.swift in Sources */, + 6E01DC2A28D683AB004B5956 /* Crypto.swift in Sources */, + 6E01DBAE28D683AB004B5956 /* Notification.swift in Sources */, + 6E01DC4628D683AB004B5956 /* Bool.swift in Sources */, + 6E01DBFA28D683AB004B5956 /* CreateNewKeyRequest.swift in Sources */, + 6E01DBE228D683AB004B5956 /* Status.swift in Sources */, + 6E01DBD228D683AB004B5956 /* POSIXTime.swift in Sources */, + 6E01DC5228D683AB004B5956 /* NewKey.swift in Sources */, + 6E01DB9228D683AB004B5956 /* SetupCharacteristic.swift in Sources */, + 6E01DC1A28D683AB004B5956 /* LockInformationRequest.swift in Sources */, + 6E01DB9628D683AB004B5956 /* DeviceManager.swift in Sources */, + 6E01DC2628D683AB004B5956 /* SecureData.swift in Sources */, + 6E01DB7E28D683AB004B5956 /* Advertisement.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1156,8 +1177,8 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = Configs/CoreLock.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.2; - MACOSX_DEPLOYMENT_TARGET = 10.11; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.CoreLock; @@ -1166,10 +1187,10 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 9.0; + TVOS_DEPLOYMENT_TARGET = 13.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 3.0; + WATCHOS_DEPLOYMENT_TARGET = 6.0; }; name = Debug; }; @@ -1216,8 +1237,8 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = Configs/CoreLock.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.2; - MACOSX_DEPLOYMENT_TARGET = 10.11; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.CoreLock; PRODUCT_NAME = CoreLock; @@ -1225,11 +1246,11 @@ SWIFT_COMPILATION_MODE = wholemodule; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 9.0; + TVOS_DEPLOYMENT_TARGET = 13.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 3.0; + WATCHOS_DEPLOYMENT_TARGET = 6.0; }; name = Release; }; @@ -1252,6 +1273,7 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -1273,6 +1295,7 @@ ); SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = YES; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -1295,6 +1318,7 @@ SDKROOT = watchos; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; }; name = Debug; @@ -1317,6 +1341,7 @@ ); SDKROOT = watchos; SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; }; name = Release; @@ -1340,6 +1365,7 @@ SDKROOT = appletvos; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; }; name = Debug; @@ -1362,6 +1388,7 @@ ); SDKROOT = appletvos; SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; }; name = Release; @@ -1387,6 +1414,7 @@ SDKROOT = macosx; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -1410,6 +1438,7 @@ ); SDKROOT = macosx; SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -1427,7 +1456,6 @@ "@loader_path/../Frameworks", $FRAMEWORK_SEARCH_PATHS, ); - MACOSX_DEPLOYMENT_TARGET = 10.11; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.CoreLockTests; PRODUCT_NAME = CoreLockTests; SDKROOT = macosx; @@ -1449,7 +1477,6 @@ "@loader_path/../Frameworks", $FRAMEWORK_SEARCH_PATHS, ); - MACOSX_DEPLOYMENT_TARGET = 10.11; PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.CoreLockTests; PRODUCT_NAME = CoreLockTests; SDKROOT = macosx; @@ -1518,22 +1545,6 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 6E621242234721FF007D49BF /* XCRemoteSwiftPackageReference "Bluetooth" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/PureSwift/Bluetooth.git"; - requirement = { - branch = master; - kind = branch; - }; - }; - 6E9C95E823114962007C18FE /* XCRemoteSwiftPackageReference "CryptoSwift" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/krzyzanowskim/CryptoSwift"; - requirement = { - branch = master; - kind = branch; - }; - }; 6E9C95EB231149A5007C18FE /* XCRemoteSwiftPackageReference "GATT" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/PureSwift/GATT.git"; @@ -1550,42 +1561,9 @@ 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 */ - 6E621243234721FF007D49BF /* Bluetooth */ = { - isa = XCSwiftPackageProductDependency; - package = 6E621242234721FF007D49BF /* XCRemoteSwiftPackageReference "Bluetooth" */; - productName = Bluetooth; - }; - 6E62124723472217007D49BF /* Bluetooth */ = { - isa = XCSwiftPackageProductDependency; - package = 6E621242234721FF007D49BF /* XCRemoteSwiftPackageReference "Bluetooth" */; - productName = Bluetooth; - }; - 6E621265234723ED007D49BF /* Bluetooth */ = { - isa = XCSwiftPackageProductDependency; - package = 6E621242234721FF007D49BF /* XCRemoteSwiftPackageReference "Bluetooth" */; - productName = Bluetooth; - }; - 6E62126923472400007D49BF /* Bluetooth */ = { - isa = XCSwiftPackageProductDependency; - package = 6E621242234721FF007D49BF /* XCRemoteSwiftPackageReference "Bluetooth" */; - productName = Bluetooth; - }; - 6E9C95E923114962007C18FE /* CryptoSwift */ = { - isa = XCSwiftPackageProductDependency; - package = 6E9C95E823114962007C18FE /* XCRemoteSwiftPackageReference "CryptoSwift" */; - productName = CryptoSwift; - }; 6E9C95EC231149A5007C18FE /* GATT */ = { isa = XCSwiftPackageProductDependency; package = 6E9C95EB231149A5007C18FE /* XCRemoteSwiftPackageReference "GATT" */; @@ -1601,31 +1579,6 @@ 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" */; - productName = CryptoSwift; - }; 6ED81FE2231662E200B69520 /* DarwinGATT */ = { isa = XCSwiftPackageProductDependency; package = 6E9C95EB231149A5007C18FE /* XCRemoteSwiftPackageReference "GATT" */; @@ -1641,11 +1594,6 @@ package = 6E9C95F0231149F5007C18FE /* XCRemoteSwiftPackageReference "TLVCoding" */; productName = TLVCoding; }; - 6ED81FE8231662F100B69520 /* CryptoSwift */ = { - isa = XCSwiftPackageProductDependency; - package = 6E9C95E823114962007C18FE /* XCRemoteSwiftPackageReference "CryptoSwift" */; - productName = CryptoSwift; - }; 6ED81FEA231662F100B69520 /* DarwinGATT */ = { isa = XCSwiftPackageProductDependency; package = 6E9C95EB231149A5007C18FE /* XCRemoteSwiftPackageReference "GATT" */; @@ -1661,11 +1609,6 @@ package = 6E9C95F0231149F5007C18FE /* XCRemoteSwiftPackageReference "TLVCoding" */; productName = TLVCoding; }; - 6ED81FF0231662FB00B69520 /* CryptoSwift */ = { - isa = XCSwiftPackageProductDependency; - package = 6E9C95E823114962007C18FE /* XCRemoteSwiftPackageReference "CryptoSwift" */; - productName = CryptoSwift; - }; 6ED81FF2231662FB00B69520 /* DarwinGATT */ = { isa = XCSwiftPackageProductDependency; package = 6E9C95EB231149A5007C18FE /* XCRemoteSwiftPackageReference "GATT" */; diff --git a/Xcode/CoreLock.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Xcode/CoreLock.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 2e8ca75d..919434a6 100644 --- a/Xcode/CoreLock.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/Xcode/CoreLock.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/Xcode/CoreLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcode/CoreLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..5be943f4 --- /dev/null +++ b/Xcode/CoreLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,32 @@ +{ + "pins" : [ + { + "identity" : "bluetooth", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/Bluetooth.git", + "state" : { + "revision" : "f28f73c1ff5e0877c212300ab356997e7e9570fa", + "version" : "6.1.0" + } + }, + { + "identity" : "gatt", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/GATT.git", + "state" : { + "branch" : "master", + "revision" : "579ffd583f9a32a88e68b69e12eb85856ee165cb" + } + }, + { + "identity" : "tlvcoding", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/TLVCoding.git", + "state" : { + "branch" : "master", + "revision" : "49eae2e68320dbe1533eb2880ee82f7b6144517a" + } + } + ], + "version" : 2 +} From 98508f85255673dd8f901fbb54e09dd25917d645 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 16:37:42 -0700 Subject: [PATCH 032/229] [lockd] Fixed Linux compilation --- Package.swift | 5 +++++ Sources/lockd/AuthorizationStoreFile.swift | 8 ++++---- Sources/lockd/GPIO.swift | 2 +- Sources/lockd/LockDaemon.swift | 6 +++--- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Package.swift b/Package.swift index ccb7b23c..9b435fb4 100644 --- a/Package.swift +++ b/Package.swift @@ -69,6 +69,11 @@ let package = Package( package: "GATT", condition: .when(platforms: [.macOS]) ), + .product( + name: "BluetoothLinux", + package: "BluetoothLinux", + condition: .when(platforms: [.linux]) + ), "CoreLockGATTServer", "SwiftyGPIO" ] diff --git a/Sources/lockd/AuthorizationStoreFile.swift b/Sources/lockd/AuthorizationStoreFile.swift index e8bb9be5..7693f5c0 100644 --- a/Sources/lockd/AuthorizationStoreFile.swift +++ b/Sources/lockd/AuthorizationStoreFile.swift @@ -55,7 +55,7 @@ public final class AuthorizationStoreFile: LockAuthorizationStore { public func key(for id: UUID) throws -> (key: Key, secret: KeyData)? { - guard let keyEntry = database.keys.first(where: { $0.key.identifier == identifier }) + guard let keyEntry = database.keys.first(where: { $0.key.id == id }) else { return nil } return (keyEntry.key, keyEntry.secret) @@ -68,7 +68,7 @@ public final class AuthorizationStoreFile: LockAuthorizationStore { public func newKey(for id: UUID) throws -> (newKey: NewKey, secret: KeyData)? { - guard let keyEntry = database.newKeys.first(where: { $0.newKey.identifier == identifier }) + guard let keyEntry = database.newKeys.first(where: { $0.newKey.id == id }) else { return nil } return (keyEntry.newKey, keyEntry.secret) @@ -76,12 +76,12 @@ public final class AuthorizationStoreFile: LockAuthorizationStore { public func removeKey(_ id: UUID) throws { - try write { $0.keys.removeAll(where: { $0.key.identifier == identifier }) } + try write { $0.keys.removeAll(where: { $0.key.id == id }) } } public func removeNewKey(_ id: UUID) throws { - try write { $0.newKeys.removeAll(where: { $0.newKey.identifier == identifier }) } + try write { $0.newKeys.removeAll(where: { $0.newKey.id == id }) } } public func removeAll() throws { diff --git a/Sources/lockd/GPIO.swift b/Sources/lockd/GPIO.swift index 5b69c858..797eb018 100755 --- a/Sources/lockd/GPIO.swift +++ b/Sources/lockd/GPIO.swift @@ -11,7 +11,7 @@ import CoreLock import CoreLockGATTServer import SwiftyGPIO -public protocol LockGPIOController: class, UnlockDelegate { +public protocol LockGPIOController: AnyObject, UnlockDelegate { var relay: GPIOState { get set } diff --git a/Sources/lockd/LockDaemon.swift b/Sources/lockd/LockDaemon.swift index e907e755..13d772d9 100644 --- a/Sources/lockd/LockDaemon.swift +++ b/Sources/lockd/LockDaemon.swift @@ -71,15 +71,15 @@ struct LockDaemon { try await Task.sleep(nanoseconds: 5 * 1_000_000_000) hostController = await HostController.default } - - let address = try await hostController!.readDeviceAddress() + let linuxHostController = self.hostController as! BluetoothLinux.HostController + let address = try await linuxHostController.readDeviceAddress() print("Bluetooth Controller: \(address)") let serverOptions = GATTPeripheralOptions( maximumTransmissionUnit: .max, maximumPreparedWrites: 1000 ) let peripheral = LinuxPeripheral( - hostController: hostController, + hostController: linuxHostController, options: serverOptions, socket: BluetoothLinux.L2CAPSocket.self ) From 4796f6457c3f0ec16acf28734275f6c2ae18dbfa Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 20:19:23 -0700 Subject: [PATCH 033/229] [LockKit] Updated for Swift 5.7 --- .../contents.xcworkspacedata | 7 - .../CoreLock/Bluetooth/DeviceManager.swift | 20 +- .../Bluetooth/KeysCharacteristic.swift | 2 +- .../Networking/CreateNewKeyRequest.swift | 2 +- Sources/CoreLock/Networking/KeysRequest.swift | 2 +- .../CoreLock/Networking/LockNetService.swift | 2 +- iOS/Intent/IntentHandler.swift | 12 +- iOS/IntentUI/IntentViewController.swift | 2 +- iOS/LockKit/Controller/Extensions/Setup.swift | 14 +- .../Controller/Extensions/Unlock.swift | 18 +- .../Controller/Extensions/Update.swift | 8 +- .../Controller/KeyViewController.swift | 4 +- .../Controller/LockEventsViewController.swift | 35 +- .../LockPermissionsViewController.swift | 51 ++- .../Controller/LockViewController.swift | 37 +- .../NewKeyRecieveViewController.swift | 67 +-- ...NewKeySelectPermissionViewController.swift | 6 +- .../ActivityIndicatorViewController.swift | 28 ++ .../Protocols/NewKeyViewController.swift | 38 +- iOS/LockKit/Model/Activity.swift | 36 +- iOS/LockKit/Model/ApplicationData.swift | 19 +- iOS/LockKit/Model/BeaconController.swift | 10 +- .../ConfirmNewKeyEventManagedObject.swift | 8 +- .../Model/CoreData/ContactManagedObject.swift | 2 +- .../CreateNewKeyEventManagedObject.swift | 8 +- .../Model/CoreData/EventManagedObject.swift | 24 +- .../Model/CoreData/KeyManagedObject.swift | 22 +- .../Model/CoreData/LockManagedObject.swift | 10 +- .../Model/CoreData/ManagedObject.swift | 25 +- .../Model/CoreData/NewKeyManagedObject.swift | 12 +- .../Model/CoreData/PersistentContainer.swift | 18 + .../RemoveKeyEventManagedObject.swift | 10 +- .../CoreData/SetupEventManagedObject.swift | 6 +- .../CoreData/UnlockEventManagedObject.swift | 6 +- iOS/LockKit/Model/CoreSpotlight.swift | 8 +- iOS/LockKit/Model/DeviceManager.swift | 20 - iOS/LockKit/Model/FileManager.swift | 2 +- iOS/LockKit/Model/Intent.swift | 8 +- iOS/LockKit/Model/Log.swift | 10 +- iOS/LockKit/Model/NetService.swift | 3 +- iOS/LockKit/Model/Queue.swift | 12 +- iOS/LockKit/Model/Shortcut.swift | 4 +- iOS/LockKit/Model/Store.swift | 398 +++++++++--------- iOS/LockKit/Model/UserActivity.swift | 2 +- .../Model/iCloud/CloudApplicationData.swift | 6 +- iOS/LockKit/Model/iCloud/CloudEvent.swift | 20 +- iOS/LockKit/Model/iCloud/CloudKey.swift | 6 +- iOS/LockKit/Model/iCloud/CloudNewKey.swift | 6 +- .../Model/iCloud/CloudNewKeyInvitation.swift | 2 +- iOS/LockKit/Model/iCloud/CloudShare.swift | 2 +- iOS/LockKit/Model/iCloud/iCloud.swift | 34 +- iOS/Message/MessagesViewController.swift | 4 +- iOS/SmartLock.xcodeproj/project.pbxproj | 132 +----- .../xcshareddata/swiftpm/Package.resolved | 213 ++++------ .../xcschemes/Watch (Complication).xcscheme | 25 +- .../xcschemes/Watch (Notification).xcscheme | 25 +- .../xcshareddata/xcschemes/Watch.xcscheme | 25 +- .../xcschemes/WatchIntent.xcscheme | 25 +- iOS/SmartLock/AppDelegate.swift | 10 +- .../Controller/KeysViewController.swift | 2 +- .../NearbyLocksViewController.swift | 34 +- iOS/Today/TodayViewController.swift | 14 +- .../Controller/InterfaceController.swift | 12 +- iOS/Watch Extension/Controller/Unlock.swift | 4 +- iOS/Watch Extension/SessionController.swift | 8 +- 65 files changed, 750 insertions(+), 897 deletions(-) delete mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a6..00000000 --- a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/Sources/CoreLock/Bluetooth/DeviceManager.swift b/Sources/CoreLock/Bluetooth/DeviceManager.swift index 99122fa5..00eb9409 100644 --- a/Sources/CoreLock/Bluetooth/DeviceManager.swift +++ b/Sources/CoreLock/Bluetooth/DeviceManager.swift @@ -133,7 +133,7 @@ public final class LockManager { try central.device(for: peripheral, timeout: timeout) { [unowned self] (cache) in - let characteristicValue = UnlockCharacteristic(identifier: key.identifier, + let characteristicValue = UnlockCharacteristic(identifier: key.id, action: action, authentication: Authentication(key: key.secret)) @@ -148,7 +148,7 @@ public final class LockManager { with key: KeyCredentials, timeout: TimeInterval = .gattDefaultTimeout) throws { - log?("Create \(newKey.permission.type) key \"\(newKey.name)\" \(newKey.identifier) for \(peripheral)") + log?("Create \(newKey.permission.type) key \"\(newKey.name)\" \(newKey.id) for \(peripheral)") let timeout = Timeout(timeout: timeout) @@ -156,7 +156,7 @@ public final class LockManager { let characteristicValue = try CreateNewKeyCharacteristic( request: newKey, - for: key.identifier, + for: key.id, sharedSecret: key.secret ) @@ -171,7 +171,7 @@ public final class LockManager { with key: KeyCredentials, timeout: TimeInterval = .gattDefaultTimeout) throws { - log?("Confirm key \(key.identifier) for \(peripheral)") + log?("Confirm key \(key.id) for \(peripheral)") let timeout = Timeout(timeout: timeout) @@ -179,7 +179,7 @@ public final class LockManager { let characteristicValue = try ConfirmNewKeyCharacteristic( request: confirmation, - for: key.identifier, + for: key.id, sharedSecret: key.secret ) @@ -201,7 +201,7 @@ public final class LockManager { try central.device(for: peripheral, timeout: timeout) { [unowned self] (cache) in - let characteristicValue = RemoveKeyCharacteristic(identifier: key.identifier, + let characteristicValue = RemoveKeyCharacteristic(identifier: key.id, key: identifier, type: type, authentication: Authentication(key: key.secret)) @@ -299,11 +299,11 @@ public final class LockManager { typealias Notification = KeysCharacteristic var keysList = KeysList() try list(write: ListKeysCharacteristic( - identifier: key.identifier, + identifier: key.id, authentication: Authentication(key: key.secret) ), notify: Notification.self, for: peripheral, with: key, timeout: timeout) { [unowned self] (notificationValue) in keysList.append(notificationValue.key) - self.log?("Recieved key \(notificationValue.key.identifier)") + self.log?("Recieved key \(notificationValue.key.id)") notification(keysList, notificationValue.isLast) } assert(keysList.isEmpty == false) @@ -336,13 +336,13 @@ public final class LockManager { var events = EventsList() events.reserveCapacity(fetchRequest?.limit.flatMap({ Int($0) }) ?? 1) try list(write: ListEventsCharacteristic( - identifier: key.identifier, + identifier: key.id, authentication: Authentication(key: key.secret), fetchRequest: fetchRequest ), notify: Notification.self, for: peripheral, with: key, timeout: timeout) { [unowned self] (notificationValue) in if let event = notificationValue.event { events.append(event) - self.log?("Recieved event \(event.identifier)") + self.log?("Recieved event \(event.id)") } notification(events, notificationValue.isLast) } diff --git a/Sources/CoreLock/Bluetooth/KeysCharacteristic.swift b/Sources/CoreLock/Bluetooth/KeysCharacteristic.swift index 866c3a8d..c6ba63c4 100644 --- a/Sources/CoreLock/Bluetooth/KeysCharacteristic.swift +++ b/Sources/CoreLock/Bluetooth/KeysCharacteristic.swift @@ -103,7 +103,7 @@ public extension KeysList { } } -internal extension KeysList { +public extension KeysList { mutating func append(_ newValue: KeyListNotification.KeyValue) { switch newValue { diff --git a/Sources/CoreLock/Networking/CreateNewKeyRequest.swift b/Sources/CoreLock/Networking/CreateNewKeyRequest.swift index 607c998a..abb7d0e5 100644 --- a/Sources/CoreLock/Networking/CreateNewKeyRequest.swift +++ b/Sources/CoreLock/Networking/CreateNewKeyRequest.swift @@ -74,7 +74,7 @@ public extension LockNetService.Client { with key: KeyCredentials, timeout: TimeInterval = LockNetService.defaultTimeout) throws { - log?("Create \(newKey.permission.type) key \"\(newKey.name)\" \(newKey.identifier) for \(server.url.absoluteString)") + log?("Create \(newKey.permission.type) key \"\(newKey.name)\" \(newKey.id) for \(server.url.absoluteString)") let request = try CreateNewKeyNetServiceRequest( server: server.url, diff --git a/Sources/CoreLock/Networking/KeysRequest.swift b/Sources/CoreLock/Networking/KeysRequest.swift index c2ee6aca..83c39a9f 100644 --- a/Sources/CoreLock/Networking/KeysRequest.swift +++ b/Sources/CoreLock/Networking/KeysRequest.swift @@ -49,7 +49,7 @@ public extension LockNetService.Client { let request = KeysNetServiceRequest( server: server.url, authorization: LockNetService.Authorization( - key: key.identifier, + key: key.id, authentication: Authentication(key: key.secret) ) ) diff --git a/Sources/CoreLock/Networking/LockNetService.swift b/Sources/CoreLock/Networking/LockNetService.swift index 7646eaca..a30f53ed 100644 --- a/Sources/CoreLock/Networking/LockNetService.swift +++ b/Sources/CoreLock/Networking/LockNetService.swift @@ -156,7 +156,7 @@ public extension LockNetService.Authorization { init(key: KeyCredentials) { - self.init(key: key.identifier, authentication: Authentication(key: key.secret)) + self.init(key: key.id, authentication: Authentication(key: key.secret)) } } diff --git a/iOS/Intent/IntentHandler.swift b/iOS/Intent/IntentHandler.swift index d6306947..69cc1d76 100644 --- a/iOS/Intent/IntentHandler.swift +++ b/iOS/Intent/IntentHandler.swift @@ -39,7 +39,7 @@ final class IntentHandler: INExtension { Store.shared.loadCache() } - log("🎙 Handle intent \(intent.intentDescription ?? intent.identifier ?? intent.description)") + log("🎙 Handle intent \(intent.intentDescription ?? intent.id ?? intent.description)") if #available(watchOSApplicationExtension 5.0, *) { return UnlockIntentHandler() @@ -82,20 +82,20 @@ final class UnlockIntentHandler: NSObject, UnlockIntentHandling { } // validate UUID string - guard let identifier = intentLock.identifier.flatMap({ UUID(uuidString: $0) }) else { + guard let identifier = intentLock.id.flatMap({ UUID(uuidString: $0) }) else { completion(.unsupported()) return } // validate key is available for lock. guard let lockCache = Store.shared[lock: identifier], - Store.shared[key: lockCache.key.identifier] != nil else { + Store.shared[key: lockCache.key.id] != nil else { completion(.unsupported(forReason: .unknownLock)) return } // check if lock is in range - var device: LockPeripheral? + var device: NativeCentral.Peripheral? do { device = try Store.shared.device(for: identifier, scanDuration: 2.0) } catch { completion(.confirmationRequired(with: intentLock)) @@ -124,7 +124,7 @@ final class UnlockIntentHandler: NSObject, UnlockIntentHandling { return } - guard let identifierString = intentLock.identifier, + guard let identifierString = intentLock.id, let lockIdentifier = UUID(uuidString: identifierString), let _ = Store.shared[lock: lockIdentifier] else { completion(.failure(failureReason: "Invalid lock.")) @@ -147,7 +147,7 @@ final class UnlockIntentHandler: NSObject, UnlockIntentHandling { return } - guard let identifierString = intentLock.identifier, + guard let identifierString = intentLock.id, let lockIdentifier = UUID(uuidString: identifierString), let lockCache = Store.shared[lock: lockIdentifier] else { completion(.failure(failureReason: "Invalid lock.")) diff --git a/iOS/IntentUI/IntentViewController.swift b/iOS/IntentUI/IntentViewController.swift index 04ebb64c..1da787b5 100644 --- a/iOS/IntentUI/IntentViewController.swift +++ b/iOS/IntentUI/IntentViewController.swift @@ -47,7 +47,7 @@ final class IntentViewController: UIViewController, INUIHostedViewControlling { Store.shared.loadCache() guard let intent = interaction.intent as? UnlockIntent, - let lockIdentifierString = intent.lock?.identifier, + let lockIdentifierString = intent.lock?.id, let lockIdentifier = UUID(uuidString: lockIdentifierString), let lockCache = FileManager.Lock.shared.applicationData?.locks[lockIdentifier] else { completion(false, [], .zero) diff --git a/iOS/LockKit/Controller/Extensions/Setup.swift b/iOS/LockKit/Controller/Extensions/Setup.swift index 4ac1b4ee..8e31a69b 100644 --- a/iOS/LockKit/Controller/Extensions/Setup.swift +++ b/iOS/LockKit/Controller/Extensions/Setup.swift @@ -15,7 +15,7 @@ import QRCodeReader public extension ActivityIndicatorViewController where Self: UIViewController { - func setup(lock: LockPeripheral) { + func setup(lock: NativeCentral.Peripheral) { // scan QR code precondition(QRCodeReader.isAvailable(), "QR Code Reader not supported") @@ -61,10 +61,10 @@ public extension ActivityIndicatorViewController where Self: UIViewController { func setup(lock id: UUID, secret: KeyData, name: String? = nil, scanDuration: TimeInterval = 2.0) { let name = name ?? R.string.localizable.newLockName() - performActivity(queue: .bluetooth, { () -> Bool in - guard let lockPeripheral = try Store.shared.device(for: identifier, scanDuration: scanDuration) + performActivity({ + guard let lockPeripheral = try await Store.shared.device(for: id, scanDuration: scanDuration) else { return false } - try Store.shared.setup(lockPeripheral, sharedSecret: secret, name: name) + try await Store.shared.setup(lockPeripheral, sharedSecret: secret, name: name) return true }, completion: { (viewController, foundDevice) in if foundDevice == false { @@ -73,11 +73,11 @@ public extension ActivityIndicatorViewController where Self: UIViewController { }) } - func setup(lock: LockPeripheral, sharedSecret: KeyData, name: String? = nil) { + func setup(lock: NativeCentral.Peripheral, sharedSecret: KeyData, name: String? = nil) { let name = name ?? R.string.localizable.newLockName() - performActivity(queue: .bluetooth, { - try Store.shared.setup(lock, sharedSecret: sharedSecret, name: name) + performActivity({ + try await Store.shared.setup(lock, sharedSecret: sharedSecret, name: name) }) } } diff --git a/iOS/LockKit/Controller/Extensions/Unlock.swift b/iOS/LockKit/Controller/Extensions/Unlock.swift index 32f7600a..82089f29 100644 --- a/iOS/LockKit/Controller/Extensions/Unlock.swift +++ b/iOS/LockKit/Controller/Extensions/Unlock.swift @@ -15,19 +15,19 @@ public extension ActivityIndicatorViewController where Self: UIViewController { func unlock(lock id: UUID, action: UnlockAction = .default, scanDuration: TimeInterval = 2.0) { - log("Unlock \(identifier)") + log("Unlock \(id)") - performActivity(queue: DispatchQueue.bluetooth, { - guard let lockPeripheral = try Store.shared.device(for: identifier, scanDuration: scanDuration) - else { throw LockError.notInRange(lock: identifier) } - try Store.shared.unlock(lockPeripheral, action: action) + performActivity({ + guard let lockPeripheral = try await Store.shared.device(for: id, scanDuration: scanDuration) + else { throw LockError.notInRange(lock: id) } + try await Store.shared.unlock(lockPeripheral, action: action) }, completion: { (viewController, _) in - log("Successfully unlocked lock \"\(identifier)\"") + log("Successfully unlocked lock \"\(id)\"") }) } - func unlock(lock: LockPeripheral, action: UnlockAction = .default) { - performActivity(queue: .bluetooth, { try Store.shared.unlock(lock, action: action) }) + func unlock(lock: NativeCentral.Peripheral, action: UnlockAction = .default) { + performActivity({ try await Store.shared.unlock(lock, action: action) }) } } @@ -45,7 +45,7 @@ public extension UIViewController { } if #available(iOS 12, iOSApplicationExtension 12.0, *) { - let intent = UnlockIntent(identifier: lock, cache: lockCache) + let intent = UnlockIntent(id: lock, cache: lockCache) let interaction = INInteraction(intent: intent, response: nil) interaction.donate { error in if let error = error { diff --git a/iOS/LockKit/Controller/Extensions/Update.swift b/iOS/LockKit/Controller/Extensions/Update.swift index 5e7b2369..075b3c7a 100644 --- a/iOS/LockKit/Controller/Extensions/Update.swift +++ b/iOS/LockKit/Controller/Extensions/Update.swift @@ -14,7 +14,7 @@ public extension ActivityIndicatorViewController where Self: UIViewController { func update(lock id: UUID) { - guard let key = Store.shared.credentials(for: identifier) + guard let key = Store.shared.credentials(for: id) else { assertionFailure(); return } let alert = UIAlertController(title: R.string.activity.updateActivityAlertTitle(), @@ -29,16 +29,16 @@ public extension ActivityIndicatorViewController where Self: UIViewController { 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 }) + guard let netService = try client.discover(duration: 2.0, timeout: 10.0).first(where: { $0.id == 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/KeyViewController.swift b/iOS/LockKit/Controller/KeyViewController.swift index dc256caf..88b9336a 100644 --- a/iOS/LockKit/Controller/KeyViewController.swift +++ b/iOS/LockKit/Controller/KeyViewController.swift @@ -56,7 +56,7 @@ public class KeyViewController: UITableViewController { .name(key.name), .permission(key.permission), .created(key.created), - .identifier(key.identifier) + .identifier(key.id) ]) ) ] @@ -78,7 +78,7 @@ public class KeyViewController: UITableViewController { .permission(newKey.permission), .created(newKey.created), .expiration(newKey.expiration), - .identifier(newKey.identifier) + .identifier(newKey.id) ]) ) ] diff --git a/iOS/LockKit/Controller/LockEventsViewController.swift b/iOS/LockKit/Controller/LockEventsViewController.swift index 22e9b272..8a35ed59 100644 --- a/iOS/LockKit/Controller/LockEventsViewController.swift +++ b/iOS/LockKit/Controller/LockEventsViewController.swift @@ -27,7 +27,7 @@ public final class LockEventsViewController: TableViewController { public lazy var activityIndicator: UIActivityIndicatorView = self.loadActivityIndicatorView() private var locks: Set { - return self.lock.flatMap { [$0] } ?? Set(Store.shared.locks.value.keys) + return self.lock.flatMap { [$0] } ?? Set(Store.shared.locks.keys) } private lazy var dateFormatter: DateFormatter = { @@ -167,14 +167,14 @@ public final class LockEventsViewController: TableViewController { let locks = self.locks let context = Store.shared.backgroundContext - performActivity(queue: .app, { [weak self] in + performActivity({ [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) + try context.find(id: lock, type: LockManagedObject.self) .flatMap { try $0.lastEvent(in: context)?.date } } let fetchRequest = FetchRequest( @@ -187,17 +187,12 @@ public final class LockEventsViewController: TableViewController { ) ) // first try via Bonjour - if let netService = try Store.shared.netServiceClient.discover(duration: 1.0, timeout: 10.0).first(where: { $0.identifier == lock }) { - + /*if let netService = try Store.shared.netServiceClient.discover(duration: 1.0, timeout: 10.0).first(where: { $0.id == 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 await Store.shared.central.state == .poweredOn, + let device = try await Store.shared.device(for: lock, scanDuration: 2.0) { + _ = try await Store.shared.listEvents(device, fetchRequest: fetchRequest) } else if expectsLock { throw LockError.notInRange(lock: lock) } else { @@ -286,17 +281,19 @@ public final class LockEventsViewController: TableViewController { private func loadKeys() { - guard Store.shared.lockManager.central.state == .poweredOn - else { return } + //guard Store.shared.central.state == .poweredOn + // else { return } let locks = self.locks.filter { Store.shared[lock: $0]?.key.permission.isAdministrator ?? false && (needsKeys.isEmpty ? true : needsKeys.contains($0)) } - performActivity(queue: .bluetooth, { - try locks.forEach { - try Store.shared.device(for: $0, scanDuration: 1.0).flatMap { - let canListKeys = try Store.shared.listKeys($0) + performActivity({ + guard await Store.shared.central.state == .poweredOn + else { return } + for id in locks { + if let peripheral = try await Store.shared.device(for: id, scanDuration: 1.0) { + let canListKeys = try await Store.shared.listKeys(peripheral) assert(canListKeys) } } diff --git a/iOS/LockKit/Controller/LockPermissionsViewController.swift b/iOS/LockKit/Controller/LockPermissionsViewController.swift index a5f3ab80..2b421896 100644 --- a/iOS/LockKit/Controller/LockPermissionsViewController.swift +++ b/iOS/LockKit/Controller/LockPermissionsViewController.swift @@ -17,7 +17,7 @@ public final class LockPermissionsViewController: UITableViewController { // MARK: - Properties - public var lockid: UUID! + public var lockIdentifier: UUID! public var completion: (() -> ())? @@ -78,7 +78,7 @@ public final class LockPermissionsViewController: UITableViewController { guard let lockIdentifier = self.lockIdentifier else { assertionFailure(); return } - performActivity(queue: .app, { + performActivity({ // get lock key guard let lockCache = Store.shared[lock: lockIdentifier] @@ -87,19 +87,19 @@ public final class LockPermissionsViewController: UITableViewController { guard lockCache.key.permission.isAdministrator else { throw LockError.notAdmin(lock: lockIdentifier) } - guard let keyData = Store.shared[key: lockCache.key.identifier] else { + guard let keyData = Store.shared[key: lockCache.key.id] else { assertionFailure("Missing from Keychain") throw LockError.noKey(lock: lockIdentifier) } let key = KeyCredentials( - identifier: lockCache.key.identifier, + id: lockCache.key.id, 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 }) { + if let netService = servers.first(where: { $0.id == lockIdentifier }) { let list = try Store.shared.netServiceClient.listKeys( for: netService, with: key, @@ -117,7 +117,8 @@ public final class LockPermissionsViewController: UITableViewController { mainQueue { [weak self] in self?.list = list } }) } - } + }*/ + }) } @@ -228,10 +229,10 @@ public final class LockPermissionsViewController: UITableViewController { let lockIdentifier = self.lockIdentifier! guard let lockCache = Store.shared[lock: lockIdentifier], - let keyData = Store.shared[key: lockCache.key.identifier] + let keyData = Store.shared[key: lockCache.key.id] else { return nil } - let key = KeyCredentials(identifier: lockCache.key.identifier, secret: keyData) + let key = KeyCredentials(id: lockCache.key.id, secret: keyData) let keyEntry = self[indexPath] @@ -254,37 +255,35 @@ public final class LockPermissionsViewController: UITableViewController { alert.dismiss(animated: true) { } - self?.performActivity(queue: .app, { + self?.performActivity({ // 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) }) { + if await Store.shared.central.state == .poweredOn, + let device = try await Store.shared.device(for: lockIdentifier, scanDuration: 2.0) { - try DispatchQueue.bluetooth.sync { - try Store.shared.lockManager.removeKey( - keyEntry.identifier, - type: keyEntry.type, - for: device.scanData.peripheral, - with: key - ) - } + try await Store.shared.central.removeKey( + keyEntry.id, + type: keyEntry.type, + using: key, + for: device + ) - } else if let netService = try Store.shared.netServiceClient.discover(duration: 1.0, timeout: 10.0).first(where: { $0.identifier == lockIdentifier }) { + }/* else if let netService = try Store.shared.netServiceClient.discover(duration: 1.0, timeout: 10.0).first(where: { $0.id == lockIdentifier }) { // try via Bonjour try Store.shared.netServiceClient.removeKey( - keyEntry.identifier, + keyEntry.id, type: keyEntry.type, for: netService, with: key, timeout: 30.0 ) - } else { + }*/ else { throw LockError.notInRange(lock: lockIdentifier) } }, completion: { (viewController, _) in - viewController.list.remove(keyEntry.identifier, type: keyEntry.type) + viewController.list.remove(keyEntry.id, type: keyEntry.type) }) })) @@ -320,8 +319,8 @@ extension LockPermissionsViewController { var id: UUID { switch self { - case let .key(value): return value.identifier - case let .newKey(value): return value.identifier + case let .key(value): return value.id + case let .newKey(value): return value.id } } diff --git a/iOS/LockKit/Controller/LockViewController.swift b/iOS/LockKit/Controller/LockViewController.swift index 941a6564..29c190f8 100644 --- a/iOS/LockKit/Controller/LockViewController.swift +++ b/iOS/LockKit/Controller/LockViewController.swift @@ -29,7 +29,7 @@ public final class LockViewController: UITableViewController { // MARK: - Properties - public var lockid: UUID! { + public var lockIdentifier: UUID! { didSet { if self.isViewLoaded { self.configureView() } } } @@ -97,7 +97,7 @@ public final class LockViewController: UITableViewController { let foundLock = Store.shared[lock: lockIdentifier] - let isScanning = Store.shared.isScanning.value == false + let isScanning = Store.shared.isScanning == false let shouldScan = foundLock == nil && isScanning == false @@ -114,7 +114,7 @@ public final class LockViewController: UITableViewController { AddSiriShortcutActivity() ] - let lockItem = LockActivityItem(identifier: lockIdentifier) + let lockItem = LockActivityItem(id: lockIdentifier) let activityItems = [lockItem] as [Any] let activityViewController = UIActivityViewController( activityItems: activityItems, @@ -125,8 +125,8 @@ public final class LockViewController: UITableViewController { } if shouldScan { - performActivity(queue: .bluetooth, { - try Store.shared.scan(duration: 1.0) + performActivity({ + try await Store.shared.scan(duration: 1.0) }, completion: { (viewController, _) in show() }) @@ -152,7 +152,7 @@ public final class LockViewController: UITableViewController { guard let lockCache = Store.shared[lock: lockIdentifier] else { assertionFailure("No stored cache for lock"); return } - guard let keyData = Store.shared[key: lockCache.key.identifier] + guard let keyData = Store.shared[key: lockCache.key.id] else { assertionFailure("No stored key for lock"); return } donateUnlockIntent(for: lockIdentifier) @@ -161,28 +161,25 @@ public final class LockViewController: UITableViewController { feedbackGenerator.impactOccurred() } - let key = KeyCredentials(identifier: lockCache.key.identifier, secret: keyData) + let key = KeyCredentials(id: lockCache.key.id, secret: keyData) unlockButton.isEnabled = false - DispatchQueue.bluetooth.async { [weak self] in - - guard let controller = self else { return } - + Task { [weak self] in // enable action button - defer { mainQueue { controller.unlockButton.isEnabled = true } } + defer { Task { await MainActor.run { self?.unlockButton.isEnabled = true } } } do { - guard let peripheral = try Store.shared.device(for: lockIdentifier, scanDuration: 1.0) else { - mainQueue { controller.showErrorAlert(R.string.error.notInRange()) } + guard let peripheral = try await Store.shared.device(for: lockIdentifier, scanDuration: 1.0) else { + await MainActor.run { self?.showErrorAlert(R.string.error.notInRange()) } return } - try Store.shared.unlock(peripheral) + try await Store.shared.unlock(peripheral) } - catch { mainQueue { controller.showErrorAlert("\(error.localizedDescription)") }; return } + catch { await MainActor.run { self?.showErrorAlert("\(error.localizedDescription)") }; return } - log("Successfully unlocked lock \"\(controller.lockIdentifier!)\"") + log("Successfully unlocked lock \"\(self!.lockIdentifier!)\"") } } @@ -214,7 +211,7 @@ public final class LockViewController: UITableViewController { self.permissionTitle.text = R.string.lockViewController.permissionTitle() self.lockIdentifierLabel.text = lockIdentifier!.uuidString - self.keyIdentifierLabel.text = lockCache.key.identifier.uuidString + self.keyIdentifierLabel.text = lockCache.key.id.uuidString self.versionLabel.text = lockCache.information.version.description self.permissionLabel.text = lockCache.key.permission.localizedText } @@ -231,12 +228,12 @@ public extension UIViewController { @discardableResult func view(lock id: UUID) -> Bool { - guard Store.shared[lock: identifier] != nil else { + guard Store.shared[lock: id] != nil else { self.showErrorAlert(R.string.error.noKey()) return false } - let viewController = LockViewController.fromStoryboard(with: identifier) + let viewController = LockViewController.fromStoryboard(with: id) show(viewController, sender: self) return true } diff --git a/iOS/LockKit/Controller/NewKeyRecieveViewController.swift b/iOS/LockKit/Controller/NewKeyRecieveViewController.swift index 01323ace..6a473028 100644 --- a/iOS/LockKit/Controller/NewKeyRecieveViewController.swift +++ b/iOS/LockKit/Controller/NewKeyRecieveViewController.swift @@ -52,9 +52,14 @@ public final class NewKeyRecieveViewController: KeyViewController { self.tableView.tableFooterView = UIView() // TODO: Observe Bluetooth State - if LockManager.shared.central.state != .poweredOn, canSave { - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in - self?.configureView() + let canSave = self.canSave + Task { + let isPoweredOff = await Store.shared.central.state != .poweredOn + if isPoweredOff, canSave { + try await Task.sleep(nanoseconds: 1 * 1_000_000_000) + await MainActor.run { [weak self] in + self?.configureView() + } } } @@ -111,26 +116,32 @@ public final class NewKeyRecieveViewController: KeyViewController { .permission(invitation.key.permission), .created(invitation.key.created), .expiration(invitation.key.expiration), - .identifier(invitation.key.identifier), + .identifier(invitation.key.id), .lock(invitation.lock) ]) ) ] // add save button if not contained in navigation controller - if canSave, - LockManager.shared.central.state == .poweredOn, - parent is UINavigationController == false { - data.append( - Section( - title: nil, - items: [ - .button(R.string.keyViewController.saveTitle(), { - ($0 as? NewKeyRecieveViewController)?.save() - }) - ] - ) - ) + let canSave = self.canSave + Task { + let isPoweredOff = await Store.shared.central.state != .poweredOn + await MainActor.run { [weak self] in + if canSave, + isPoweredOff, + self?.parent is UINavigationController == false { + data.append( + Section( + title: nil, + items: [ + .button(R.string.keyViewController.saveTitle(), { + ($0 as? NewKeyRecieveViewController)?.save() + }) + ] + ) + ) + } + } } self.data = data @@ -157,26 +168,28 @@ public final class NewKeyRecieveViewController: KeyViewController { let keyData = KeyData() showActivity() - DispatchQueue.bluetooth.async { [weak self] in + Task { [weak self] in guard let controller = self else { return } do { // scan for lock if neccesary - guard let device = try Store.shared.device(for: newKeyInvitation.lock, scanDuration: 2.0), - let information = Store.shared.lockInformation.value[device.scanData.peripheral] + guard let device = try await Store.shared.device(for: newKeyInvitation.lock, scanDuration: 2.0), + let information = Store.shared.lockInformation[device] else { throw CentralError.unknownPeripheral } // recieve new key let credentials = KeyCredentials( - identifier: newKeyInvitation.key.identifier, + id: newKeyInvitation.key.id, secret: newKeyInvitation.secret ) - try LockManager.shared.confirmKey(.init(secret: keyData), - for: device.scanData.peripheral, - with: credentials) + try await Store.shared.central.confirmKey( + .init(secret: keyData), + using: credentials, + for: device + ) // update UI mainQueue { @@ -184,17 +197,17 @@ public final class NewKeyRecieveViewController: KeyViewController { // save to cache let lockCache = LockCache( key: Key( - identifier: newKeyInvitation.key.identifier, + id: newKeyInvitation.key.id, name: newKeyInvitation.key.name, created: newKeyInvitation.key.created, permission: newKeyInvitation.key.permission ), name: R.string.localizable.newLockName(), - information: .init(characteristic: information) + information: .init(information) ) Store.shared[lock: newKeyInvitation.lock] = lockCache - Store.shared[key: newKeyInvitation.key.identifier] = keyData + Store.shared[key: newKeyInvitation.key.id] = keyData controller.hideActivity(animated: true) controller.configureView() controller.completion?(true) diff --git a/iOS/LockKit/Controller/NewKeySelectPermissionViewController.swift b/iOS/LockKit/Controller/NewKeySelectPermissionViewController.swift index 5d549507..59ab6c8c 100644 --- a/iOS/LockKit/Controller/NewKeySelectPermissionViewController.swift +++ b/iOS/LockKit/Controller/NewKeySelectPermissionViewController.swift @@ -19,7 +19,7 @@ public final class NewKeySelectPermissionViewController: UITableViewController, public var completion: (((invitation: NewKey.Invitation, sender: PopoverPresentingView)?) -> ())? - public var lockid: UUID! + public var lockIdentifier: UUID! public var progressHUD: JGProgressHUD? @@ -180,14 +180,14 @@ public extension UIViewController { func shareKey(lock id: UUID, completion: @escaping (((invitation: NewKey.Invitation, sender: PopoverPresentingView)?) -> ())) { - let newKeyViewController = NewKeySelectPermissionViewController.fromStoryboard(with: identifier, completion: completion) + let newKeyViewController = NewKeySelectPermissionViewController.fromStoryboard(with: id, completion: completion) let navigationController = UINavigationController(rootViewController: newKeyViewController) self.present(navigationController, animated: true, completion: nil) } func shareKey(lock id: UUID) { - self.shareKey(lock: identifier) { [weak self] in + self.shareKey(lock: id) { [weak self] in guard let self = self else { return } guard let (invitation, sender) = $0 else { self.dismiss(animated: true, completion: nil) diff --git a/iOS/LockKit/Controller/Protocols/ActivityIndicatorViewController.swift b/iOS/LockKit/Controller/Protocols/ActivityIndicatorViewController.swift index b0357a3a..76175faa 100644 --- a/iOS/LockKit/Controller/Protocols/ActivityIndicatorViewController.swift +++ b/iOS/LockKit/Controller/Protocols/ActivityIndicatorViewController.swift @@ -55,6 +55,34 @@ public extension ActivityIndicatorViewController { } } } + + func performActivity ( + showActivity: Bool = true, + _ asyncOperation: @escaping () async throws -> T, + completion: ((Self, T) -> ())? = nil + ) { + assert(Thread.isMainThread) + if showActivity { self.showActivity() } + Task { + do { + let value = try await asyncOperation() + await MainActor.run { + if showActivity { self.hideActivity(animated: true) } + completion?(self, value) + } + } catch { + await MainActor.run { + if showActivity { self.hideActivity(animated: false) } + // show error + log("⚠️ Error: \(error.localizedDescription)") + #if DEBUG + print(error) + #endif + (self as? UIViewController)?.showErrorAlert(error.localizedDescription) + } + } + } + } } public protocol TableViewActivityIndicatorViewController: ActivityIndicatorViewController { diff --git a/iOS/LockKit/Controller/Protocols/NewKeyViewController.swift b/iOS/LockKit/Controller/Protocols/NewKeyViewController.swift index 5059260b..9ab7c2a4 100644 --- a/iOS/LockKit/Controller/Protocols/NewKeyViewController.swift +++ b/iOS/LockKit/Controller/Protocols/NewKeyViewController.swift @@ -12,7 +12,7 @@ import CoreLock public protocol NewKeyViewController: ActivityIndicatorViewController { - var lockid: UUID! { get } + var lockIdentifier: UUID! { get } var view: UIView! { get } @@ -32,7 +32,7 @@ public extension NewKeyViewController { guard let lockIdentifier = self.lockIdentifier, let lockCache = Store.shared[lock: lockIdentifier], - let parentKeyData = Store.shared[key: lockCache.key.identifier] + let parentKeyData = Store.shared[key: lockCache.key.id] else { assertionFailure(); return } // request name @@ -40,19 +40,17 @@ public extension NewKeyViewController { let newKeyIdentifier = UUID() - let parentKey = KeyCredentials(identifier: lockCache.key.identifier, secret: parentKeyData) + let parentKey = KeyCredentials(id: lockCache.key.id, secret: parentKeyData) log("Setting up new key for lock \(lockIdentifier)") self.showActivity() // add new key to lock - DispatchQueue.app.async { [weak self] in - - guard let self = self else { return } - + Task { + let newKey = NewKey( - identifier: newKeyIdentifier, + id: newKeyIdentifier, name: newKeyName, permission: permission ) @@ -71,19 +69,16 @@ public extension NewKeyViewController { do { // 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) }) { + if await Store.shared.central.state == .poweredOn, + let peripheral = try await 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 - ) - } + try await Store.shared.central.createKey( + newKeyRequest, + using: parentKey, + for: peripheral + ) - } else if let netService = try Store.shared.netServiceClient.discover(duration: 1.0, timeout: 10.0).first(where: { $0.identifier == lockIdentifier }) { + } /*else if let netService = try Store.shared.netServiceClient.discover(duration: 1.0, timeout: 10.0).first(where: { $0.id == lockIdentifier }) { // try via Bonjour try Store.shared.netServiceClient.createKey( @@ -93,7 +88,7 @@ public extension NewKeyViewController { timeout: 30.0 ) - } else { + } */ else { // not in range mainQueue { self.hideActivity(animated: false) @@ -114,8 +109,7 @@ public extension NewKeyViewController { return } - log("Created new key \(newKey.identifier) (\(newKey.permission.type))") - + log("Created new key \(newKey.id) (\(newKey.permission.type))") mainQueue { completion(newKeyInvitation) } } } diff --git a/iOS/LockKit/Model/Activity.swift b/iOS/LockKit/Model/Activity.swift index 2eeeeafe..3dff7f08 100644 --- a/iOS/LockKit/Model/Activity.swift +++ b/iOS/LockKit/Model/Activity.swift @@ -41,7 +41,7 @@ public final class LockActivityItem: NSObject { // MARK: - Activity Values public var lock: LockCache? { - return Store.shared[lock: identifier] + return Store.shared[lock: id] } public var text: String { @@ -187,7 +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] + let lockCache = Store.shared[lock: lockItem.id] else { return false } // only owner and admin can share keys @@ -200,7 +200,7 @@ public final class NewKeyActivity: UIActivity { public override var activityViewController: UIViewController? { - let viewController = NewKeySelectPermissionViewController.fromStoryboard(with: item.identifier) + let viewController = NewKeySelectPermissionViewController.fromStoryboard(with: item.id) viewController.completion = { [unowned self] in guard let (invitation, sender) = $0 else { self.activityDidFinish(false) @@ -238,7 +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] + let lockCache = Store.shared[lock: lockItem.id] else { return false } return lockCache.key.permission.isAdministrator @@ -251,7 +251,7 @@ public final class ManageKeysActivity: UIActivity { public override var activityViewController: UIViewController? { let viewController = LockPermissionsViewController.fromStoryboard( - with: item.identifier, + with: item.id, completion: { [weak self] in self?.activityDidFinish(true) } ) @@ -297,7 +297,7 @@ public final class DeleteLockActivity: UIActivity { public override var activityViewController: UIViewController? { - return type(of: self).viewController(for: item.identifier, completion: { [weak self] (didDelete) in + return type(of: self).viewController(for: item.id, completion: { [weak self] (didDelete) in self?.activityDidFinish(didDelete) if didDelete { self?.completion?() } }) @@ -357,7 +357,7 @@ public final class RenameActivity: UIActivity { } public override var activityViewController: UIViewController? { - return type(of: self).viewController(for: item.identifier, completion: { [weak self] in + return type(of: self).viewController(for: item.id, completion: { [weak self] in self?.activityDidFinish($0) }) } @@ -407,7 +407,7 @@ 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] + let lockCache = Store.shared[lock: lockItem.id] else { return false } guard lockCache.key.permission.isAdministrator @@ -433,7 +433,7 @@ public final class UpdateActivity: UIActivity { alert.dismiss(animated: true) { self.activityDidFinish(false) } })) - + /* alert.addAction(UIAlertAction(title: R.string.activity.updateActivityAlertUpdate(), style: .`default`, handler: { (UIAlertAction) in let progressHUD = JGProgressHUD(style: .dark) @@ -453,11 +453,11 @@ public final class UpdateActivity: UIActivity { } // fetch cache - guard let lockCache = Store.shared[lock: lockItem.identifier], - let keyData = Store.shared[key: lockCache.key.identifier] + guard let lockCache = Store.shared[lock: lockItem.id], + let keyData = Store.shared[key: lockCache.key.id] else { alert.dismiss(animated: true) { self.activityDidFinish(false) }; return } - let key = KeyCredentials(identifier: lockCache.key.identifier, secret: keyData) + let key = KeyCredentials(id: lockCache.key.id, secret: keyData) showProgressHUD() @@ -466,8 +466,8 @@ public final class UpdateActivity: UIActivity { let client = Store.shared.netServiceClient 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) } + guard let netService = try client.discover(duration: 1.0, timeout: 10.0).first(where: { $0.id == lockItem.id }) + else { throw LockError.notInRange(lock: lockItem.id) } try client.update(for: netService, with: key, timeout: 30.0) } @@ -488,7 +488,7 @@ public final class UpdateActivity: UIActivity { } } })) - + */ return alert } } @@ -588,7 +588,7 @@ public final class AddSiriShortcutActivity: UIActivity { else { return false } guard let lockItem = activityItems.compactMap({ $0 as? LockActivityItem }).first, - let _ = Store.shared[lock: lockItem.identifier] + let _ = Store.shared[lock: lockItem.id] else { return false } return true @@ -610,11 +610,11 @@ public final class AddSiriShortcutActivity: UIActivity { guard let lockItem = self.item else { assertionFailure(); return nil } - guard let lockCache = Store.shared[lock: lockItem.identifier] + guard let lockCache = Store.shared[lock: lockItem.id] else { assertionFailure("Invalid lock"); return nil } return INUIAddVoiceShortcutViewController( - unlock: lockItem.identifier, + unlock: lockItem.id, cache: lockCache, delegate: self ) diff --git a/iOS/LockKit/Model/ApplicationData.swift b/iOS/LockKit/Model/ApplicationData.swift index 49f83a89..c23f5d99 100644 --- a/iOS/LockKit/Model/ApplicationData.swift +++ b/iOS/LockKit/Model/ApplicationData.swift @@ -33,7 +33,7 @@ public struct ApplicationData: Codable, Equatable { /// Initialize a new application data. public init() { - self.identifier = UUID() + self.id = UUID() self.created = Date() self.updated = Date() self.locks = [:] @@ -58,8 +58,8 @@ public extension ApplicationData { } subscript (lock id: UUID) -> LockCache? { - get { return locks[identifier] } - set { locks[identifier] = newValue } + get { return locks[id] } + set { locks[id] = newValue } } subscript (key id: UUID) -> Key? { @@ -67,7 +67,7 @@ public extension ApplicationData { .lazy .map { $0.key } .lazy - .first { $0.identifier == identifier } + .first { $0.id == id } } } @@ -121,3 +121,14 @@ internal extension LockCache.Information { self.unlockActions = Set(characteristic.unlockActions) } } + +internal extension LockCache.Information { + + init(_ lock: LockInformation) { + + self.buildVersion = lock.buildVersion + self.version = lock.version + self.status = lock.status + self.unlockActions = lock.unlockActions + } +} diff --git a/iOS/LockKit/Model/BeaconController.swift b/iOS/LockKit/Model/BeaconController.swift index 6b04814a..3f0faa90 100644 --- a/iOS/LockKit/Model/BeaconController.swift +++ b/iOS/LockKit/Model/BeaconController.swift @@ -88,7 +88,7 @@ public final class BeaconController { @discardableResult public func scanBeacon(for id: UUID) -> Bool { - guard let region = beacons[identifier]?.region + guard let region = beacons[id]?.region else { return false } scanBeacons(in: region) return true @@ -379,7 +379,7 @@ internal extension CLLocationManager { startRangingBeacons(satisfying: .init(uuid: uuid)) } else { #if !targetEnvironment(macCatalyst) - startRangingBeacons(in: CLBeaconRegion(proximityUUID: uuid, id: UUID.uuidString)) + startRangingBeacons(in: CLBeaconRegion(proximityUUID: uuid, identifier: uuid.uuidString)) #endif } } @@ -389,7 +389,7 @@ internal extension CLLocationManager { stopRangingBeacons(satisfying: .init(uuid: uuid)) } else { #if !targetEnvironment(macCatalyst) - stopRangingBeacons(in: CLBeaconRegion(proximityUUID: uuid, id: UUID.uuidString)) + stopRangingBeacons(in: CLBeaconRegion(proximityUUID: uuid, identifier: uuid.uuidString)) #endif } } @@ -399,9 +399,9 @@ internal extension CLBeaconRegion { convenience init(uuid id: UUID) { if #available(iOS 13.0, iOSApplicationExtension 13.0, *) { - self.init(beaconIdentityConstraint: .init(uuid: identifier), identifier: identifier.uuidString) + self.init(beaconIdentityConstraint: .init(uuid: id), identifier: id.uuidString) } else { - self.init(proximityUUID: identifier, identifier: identifier.uuidString) + self.init(proximityUUID: id, identifier: id.uuidString) } } } diff --git a/iOS/LockKit/Model/CoreData/ConfirmNewKeyEventManagedObject.swift b/iOS/LockKit/Model/CoreData/ConfirmNewKeyEventManagedObject.swift index 4eb7b784..4f97d530 100644 --- a/iOS/LockKit/Model/CoreData/ConfirmNewKeyEventManagedObject.swift +++ b/iOS/LockKit/Model/CoreData/ConfirmNewKeyEventManagedObject.swift @@ -19,7 +19,7 @@ public final class ConfirmNewKeyEventManagedObject: EventManagedObject { context: NSManagedObjectContext) { self.init(context: context) - self.identifier = value.identifier + self.identifier = value.id self.lock = lock self.date = value.date self.key = value.key @@ -31,13 +31,13 @@ internal extension LockEvent.ConfirmNewKey { init?(managedObject: ConfirmNewKeyEventManagedObject) { - guard let identifier = managedObject.identifier, + guard let id = managedObject.identifier, let date = managedObject.date, let key = managedObject.key, let pendingKey = managedObject.pendingKey else { return nil } - self.init(identifier: identifier, date: date, newKey: pendingKey, key: key) + self.init(id: id, date: date, newKey: pendingKey, key: key) } } @@ -56,7 +56,7 @@ public extension ConfirmNewKeyEventManagedObject { assertionFailure("Missing key value") return nil } - return try context.find(identifier: newKey, type: NewKeyManagedObject.self) + return try context.find(id: newKey, type: NewKeyManagedObject.self) } /// Fetch the removed key specified by the event. diff --git a/iOS/LockKit/Model/CoreData/ContactManagedObject.swift b/iOS/LockKit/Model/CoreData/ContactManagedObject.swift index 368be0f0..f5e4bd78 100644 --- a/iOS/LockKit/Model/CoreData/ContactManagedObject.swift +++ b/iOS/LockKit/Model/CoreData/ContactManagedObject.swift @@ -17,7 +17,7 @@ public final class ContactManagedObject: NSManagedObject { internal convenience init(identifier: String, context: NSManagedObjectContext) { self.init(context: context) - self.id = id + self.identifier = identifier } internal static func find(_ identifier: String, in context: NSManagedObjectContext) throws -> ContactManagedObject? { diff --git a/iOS/LockKit/Model/CoreData/CreateNewKeyEventManagedObject.swift b/iOS/LockKit/Model/CoreData/CreateNewKeyEventManagedObject.swift index 49a4d25f..428d352f 100644 --- a/iOS/LockKit/Model/CoreData/CreateNewKeyEventManagedObject.swift +++ b/iOS/LockKit/Model/CoreData/CreateNewKeyEventManagedObject.swift @@ -17,7 +17,7 @@ public final class CreateNewKeyEventManagedObject: EventManagedObject { internal convenience init(_ value: LockEvent.CreateNewKey, lock: LockManagedObject, context: NSManagedObjectContext) { self.init(context: context) - self.identifier = value.identifier + self.identifier = value.id self.lock = lock self.date = value.date self.key = value.key @@ -29,13 +29,13 @@ public extension LockEvent.CreateNewKey { init?(managedObject: CreateNewKeyEventManagedObject) { - guard let identifier = managedObject.identifier, + guard let id = managedObject.identifier, let date = managedObject.date, let key = managedObject.key, let pendingKey = managedObject.pendingKey else { return nil } - self.init(identifier: identifier, date: date, key: key, newKey: pendingKey) + self.init(id: id, date: date, key: key, newKey: pendingKey) } } @@ -54,7 +54,7 @@ public extension CreateNewKeyEventManagedObject { assertionFailure("Missing key value") return nil } - return try context.find(identifier: newKey, type: NewKeyManagedObject.self) + return try context.find(id: newKey, type: NewKeyManagedObject.self) } func confirmKeyEvent(in context: NSManagedObjectContext) throws -> ConfirmNewKeyEventManagedObject? { diff --git a/iOS/LockKit/Model/CoreData/EventManagedObject.swift b/iOS/LockKit/Model/CoreData/EventManagedObject.swift index 41f162ff..bab923e1 100644 --- a/iOS/LockKit/Model/CoreData/EventManagedObject.swift +++ b/iOS/LockKit/Model/CoreData/EventManagedObject.swift @@ -32,7 +32,7 @@ public class EventManagedObject: NSManagedObject { internal static func find(_ id: UUID, in context: NSManagedObjectContext) throws -> EventManagedObject? { - try context.find(identifier: identifier as NSUUID, + try context.find(identifier: id as NSUUID, propertyName: #keyPath(EventManagedObject.identifier), type: EventManagedObject.self) } @@ -82,7 +82,7 @@ public extension EventManagedObject { return nil } - return try context.find(identifier: key, type: KeyManagedObject.self) + return try context.find(id: key, type: KeyManagedObject.self) } } @@ -118,24 +118,28 @@ internal extension NSManagedObjectContext { @discardableResult func insert(_ events: [LockEvent], for lock: LockManagedObject) throws -> [EventManagedObject] { - - // insert events return try events.map { - try EventManagedObject.find($0.identifier, in: self) - ?? EventManagedObject.initWith($0, lock: lock, context: self) + try insert($0, for: lock) } } + @discardableResult + func insert(_ event: LockEvent, for lock: LockManagedObject) throws -> EventManagedObject { + try EventManagedObject.find(event.id, in: self) + ?? EventManagedObject.initWith(event, lock: lock, context: self) + } + @discardableResult func insert(_ events: [LockEvent], for lock: UUID) throws -> [EventManagedObject] { - - let managedObject = try find(identifier: lock, type: LockManagedObject.self) - ?? LockManagedObject(identifier: lock, name: "", context: self) + let managedObject = try find(id: lock, type: LockManagedObject.self) + ?? LockManagedObject(id: lock, name: "", context: self) return try insert(events, for: managedObject) } @discardableResult func insert(_ event: LockEvent, for lock: UUID) throws -> EventManagedObject { - return try insert([event], for: lock)[0] + let managedObject = try find(id: lock, type: LockManagedObject.self) + ?? LockManagedObject(id: lock, name: "", context: self) + return try insert(event, for: managedObject) } } diff --git a/iOS/LockKit/Model/CoreData/KeyManagedObject.swift b/iOS/LockKit/Model/CoreData/KeyManagedObject.swift index db11c422..55121b3b 100644 --- a/iOS/LockKit/Model/CoreData/KeyManagedObject.swift +++ b/iOS/LockKit/Model/CoreData/KeyManagedObject.swift @@ -14,7 +14,7 @@ public final class KeyManagedObject: NSManagedObject { internal convenience init(_ value: Key, lock: LockManagedObject, context: NSManagedObjectContext) { self.init(context: context) - self.identifier = value.identifier + self.identifier = value.id self.lock = lock self.name = value.name self.created = value.created @@ -29,7 +29,7 @@ public extension Key { init?(managedObject: KeyManagedObject) { - guard let identifier = managedObject.identifier, + guard let id = managedObject.identifier, let name = managedObject.name, let created = managedObject.created, let permissionType = PermissionType(rawValue: numericCast(managedObject.permission)) @@ -50,7 +50,7 @@ public extension Key { } self.init( - identifier: identifier, + id: id, name: name, created: created, permission: permission @@ -69,7 +69,7 @@ internal extension NSManagedObjectContext { @discardableResult func insert(_ key: Key, for lock: LockManagedObject) throws -> KeyManagedObject { - if let managedObject = try find(identifier: key.identifier, type: KeyManagedObject.self) { + if let managedObject = try find(id: key.id, type: KeyManagedObject.self) { assert(managedObject.lock == lock, "Key stored with conflicting lock") return managedObject } else { @@ -80,8 +80,18 @@ internal extension NSManagedObjectContext { @discardableResult func insert(_ key: Key, for lock: UUID) throws -> KeyManagedObject { - let managedObject = try find(identifier: lock, type: LockManagedObject.self) - ?? LockManagedObject(identifier: lock, name: "", context: self) + let managedObject = try find(id: lock, type: LockManagedObject.self) + ?? LockManagedObject(id: lock, name: "", context: self) return try insert(key, for: managedObject) } + + @discardableResult + func insert(_ key: KeyListNotification.KeyValue, for lock: UUID) throws -> NSManagedObject { + switch key { + case let .key(key): + return try insert(key, for: lock) + case let .newKey(key): + return try insert(key, for: lock) + } + } } diff --git a/iOS/LockKit/Model/CoreData/LockManagedObject.swift b/iOS/LockKit/Model/CoreData/LockManagedObject.swift index f010b0ec..98793945 100644 --- a/iOS/LockKit/Model/CoreData/LockManagedObject.swift +++ b/iOS/LockKit/Model/CoreData/LockManagedObject.swift @@ -18,7 +18,7 @@ public final class LockManagedObject: NSManagedObject { context: NSManagedObjectContext) { self.init(context: context) - self.id = id + self.identifier = id self.name = name if let information = information { update(information: information, context: context) @@ -66,12 +66,12 @@ internal extension NSManagedObjectContext { // insert locks return try locks.map { (identifier, cache) in - if let managedObject = try find(identifier: identifier, type: LockManagedObject.self) { + if let managedObject = try find(id: identifier, type: LockManagedObject.self) { managedObject.name = cache.name managedObject.update(information: cache.information, context: self) return managedObject } else { - return LockManagedObject(identifier: identifier, + return LockManagedObject(id: identifier, name: cache.name, information: cache.information, context: self) @@ -85,11 +85,11 @@ internal extension NSManagedObjectContext { // insert lock let lockManagedObject: LockManagedObject - if let managedObject = try find(identifier: cloudValue.id.rawValue, type: LockManagedObject.self) { + if let managedObject = try find(id: cloudValue.id.rawValue, type: LockManagedObject.self) { managedObject.name = cloudValue.name lockManagedObject = managedObject } else { - lockManagedObject = LockManagedObject(identifier: cloudValue.id.rawValue, + lockManagedObject = LockManagedObject(id: cloudValue.id.rawValue, name: cloudValue.name, context: self) } diff --git a/iOS/LockKit/Model/CoreData/ManagedObject.swift b/iOS/LockKit/Model/CoreData/ManagedObject.swift index 1a34f986..417bf6f9 100644 --- a/iOS/LockKit/Model/CoreData/ManagedObject.swift +++ b/iOS/LockKit/Model/CoreData/ManagedObject.swift @@ -59,6 +59,27 @@ internal extension NSManagedObjectContext { } } } + + func commit(_ block: @escaping (NSManagedObjectContext) throws -> ()) async { + + assert(concurrencyType == .privateQueueConcurrencyType) + await perform { [unowned self] in + self.reset() + do { + try block(self) + if self.hasChanges { + try self.save() + } + } catch { + log("⚠️ Unable to commit changes: \(error.localizedDescription)") + #if DEBUG + print(error) + #endif + assertionFailure("Core Data error") + return + } + } + } } internal extension NSManagedObjectContext { @@ -77,7 +98,7 @@ internal extension NSManagedObjectContext { public protocol IdentifiableManagedObject { - var id: UUID? { get } + var identifier: UUID? { get } } public extension NSManagedObjectContext { @@ -86,7 +107,7 @@ public extension NSManagedObjectContext { let fetchRequest = NSFetchRequest() fetchRequest.entity = T.entity() - fetchRequest.predicate = NSPredicate(format: "%K == %@", "identifier", identifier as NSUUID) + fetchRequest.predicate = NSPredicate(format: "%K == %@", "identifier", id as NSUUID) fetchRequest.fetchLimit = 1 fetchRequest.includesSubentities = true fetchRequest.returnsObjectsAsFaults = false diff --git a/iOS/LockKit/Model/CoreData/NewKeyManagedObject.swift b/iOS/LockKit/Model/CoreData/NewKeyManagedObject.swift index 7414ce75..5d58ee62 100644 --- a/iOS/LockKit/Model/CoreData/NewKeyManagedObject.swift +++ b/iOS/LockKit/Model/CoreData/NewKeyManagedObject.swift @@ -14,7 +14,7 @@ public final class NewKeyManagedObject: NSManagedObject { internal convenience init(_ value: NewKey, lock: LockManagedObject, context: NSManagedObjectContext) { self.init(context: context) - self.identifier = value.identifier + self.identifier = value.id self.lock = lock self.name = value.name self.created = value.created @@ -30,7 +30,7 @@ public extension NewKey { init?(managedObject: NewKeyManagedObject) { - guard let identifier = managedObject.identifier, + guard let id = managedObject.identifier, let name = managedObject.name, let created = managedObject.created, let permissionType = PermissionType(rawValue: numericCast(managedObject.permission)), @@ -52,7 +52,7 @@ public extension NewKey { } self.init( - identifier: identifier, + id: id, name: name, permission: permission, created: created, @@ -72,7 +72,7 @@ internal extension NSManagedObjectContext { @discardableResult func insert(_ newKey: NewKey, for lock: LockManagedObject) throws -> NewKeyManagedObject { - if let managedObject = try find(identifier: newKey.identifier, type: NewKeyManagedObject.self) { + if let managedObject = try find(id: newKey.id, type: NewKeyManagedObject.self) { assert(managedObject.lock == lock, "Key stored with conflicting lock") return managedObject } else { @@ -83,8 +83,8 @@ internal extension NSManagedObjectContext { @discardableResult func insert(_ key: NewKey, for lock: UUID) throws -> NewKeyManagedObject { - let managedObject = try find(identifier: lock, type: LockManagedObject.self) - ?? LockManagedObject(identifier: lock, name: "Lock", context: self) + let managedObject = try find(id: lock, type: LockManagedObject.self) + ?? LockManagedObject(id: lock, name: "Lock", context: self) return try insert(key, for: managedObject) } } diff --git a/iOS/LockKit/Model/CoreData/PersistentContainer.swift b/iOS/LockKit/Model/CoreData/PersistentContainer.swift index f5ec3c1f..f7f674dc 100644 --- a/iOS/LockKit/Model/CoreData/PersistentContainer.swift +++ b/iOS/LockKit/Model/CoreData/PersistentContainer.swift @@ -43,4 +43,22 @@ internal extension NSPersistentContainer { } } } + + func commit(_ block: @escaping (NSManagedObjectContext) throws -> ()) async { + await performBackgroundTask { + do { + try block($0) + if $0.hasChanges { + try $0.save() + } + } catch { + log("⚠️ Unable to commit changes: \(error.localizedDescription)") + #if DEBUG + print(error) + #endif + assertionFailure("Core Data error") + return + } + } + } } diff --git a/iOS/LockKit/Model/CoreData/RemoveKeyEventManagedObject.swift b/iOS/LockKit/Model/CoreData/RemoveKeyEventManagedObject.swift index df7b844a..31034040 100644 --- a/iOS/LockKit/Model/CoreData/RemoveKeyEventManagedObject.swift +++ b/iOS/LockKit/Model/CoreData/RemoveKeyEventManagedObject.swift @@ -17,7 +17,7 @@ public final class RemoveKeyEventManagedObject: EventManagedObject { internal convenience init(_ value: LockEvent.RemoveKey, lock: LockManagedObject, context: NSManagedObjectContext) { self.init(context: context) - self.identifier = value.identifier + self.identifier = value.id self.lock = lock self.date = value.date self.key = value.key @@ -30,14 +30,14 @@ internal extension LockEvent.RemoveKey { init?(managedObject: RemoveKeyEventManagedObject) { - guard let identifier = managedObject.identifier, + guard let id = managedObject.identifier, let date = managedObject.date, let key = managedObject.key, let removedKey = managedObject.removedKey, let type = KeyType(rawValue: numericCast(managedObject.type)) else { return nil } - self.init(identifier: identifier, date: date, key: key, removedKey: removedKey, type: type) + self.init(id: id, date: date, key: key, removedKey: removedKey, type: type) } } @@ -64,11 +64,11 @@ public extension RemoveKeyEventManagedObject { switch type { case .key: - guard let managedObject = try context.find(identifier: removedKey, type: KeyManagedObject.self) + guard let managedObject = try context.find(id: removedKey, type: KeyManagedObject.self) else { return nil } return .key(managedObject) case .newKey: - guard let managedObject = try context.find(identifier: removedKey, type: NewKeyManagedObject.self) + guard let managedObject = try context.find(id: removedKey, type: NewKeyManagedObject.self) else { return nil } return .newKey(managedObject) } diff --git a/iOS/LockKit/Model/CoreData/SetupEventManagedObject.swift b/iOS/LockKit/Model/CoreData/SetupEventManagedObject.swift index dcb4e8bb..a4ae68f6 100644 --- a/iOS/LockKit/Model/CoreData/SetupEventManagedObject.swift +++ b/iOS/LockKit/Model/CoreData/SetupEventManagedObject.swift @@ -17,7 +17,7 @@ public final class SetupEventManagedObject: EventManagedObject { internal convenience init(_ value: LockEvent.Setup, lock: LockManagedObject, context: NSManagedObjectContext) { self.init(context: context) - self.identifier = value.identifier + self.identifier = value.id self.lock = lock self.date = value.date self.key = value.key @@ -28,12 +28,12 @@ public extension LockEvent.Setup { init?(managedObject: SetupEventManagedObject) { - guard let identifier = managedObject.identifier, + guard let id = managedObject.identifier, let date = managedObject.date, let key = managedObject.key else { return nil } - self.init(identifier: identifier, date: date, key: key) + self.init(id: id, date: date, key: key) } } diff --git a/iOS/LockKit/Model/CoreData/UnlockEventManagedObject.swift b/iOS/LockKit/Model/CoreData/UnlockEventManagedObject.swift index ad982ad2..c6224dfa 100644 --- a/iOS/LockKit/Model/CoreData/UnlockEventManagedObject.swift +++ b/iOS/LockKit/Model/CoreData/UnlockEventManagedObject.swift @@ -17,7 +17,7 @@ public final class UnlockEventManagedObject: EventManagedObject { internal convenience init(_ value: LockEvent.Unlock, lock: LockManagedObject, context: NSManagedObjectContext) { self.init(context: context) - self.identifier = value.identifier + self.identifier = value.id self.lock = lock self.date = value.date self.key = value.key @@ -29,13 +29,13 @@ public extension LockEvent.Unlock { init?(managedObject: UnlockEventManagedObject) { - guard let identifier = managedObject.identifier, + guard let id = managedObject.identifier, let date = managedObject.date, let key = managedObject.key, let action = UnlockAction(rawValue: numericCast(managedObject.action)) else { return nil } - self.init(identifier: identifier, date: date, key: key, action: action) + self.init(id: id, date: date, key: key, action: action) } } diff --git a/iOS/LockKit/Model/CoreSpotlight.swift b/iOS/LockKit/Model/CoreSpotlight.swift index ed798f94..86551433 100644 --- a/iOS/LockKit/Model/CoreSpotlight.swift +++ b/iOS/LockKit/Model/CoreSpotlight.swift @@ -40,7 +40,7 @@ public final class SpotlightController { let searchableItems = locks .lazy - .map { SearchableLock(identifier: $0.key, cache: $0.value) } + .map { SearchableLock(id: $0.key, cache: $0.value) } .map { $0.searchableItem() } index.deleteSearchableItems(withDomainIdentifiers: [SearchableLock.searchDomain]) { [weak self] (error) in @@ -84,7 +84,7 @@ public final class SpotlightController { case let .lock(lock): let searchIdentifier = SearchableLock.searchIdentifier(for: lock) if let cache = locks[lock] { - let item = SearchableLock(identifier: lock, cache: cache).searchableItem() + let item = SearchableLock(id: lock, cache: cache).searchableItem() searchableItems.append(item) } else { deletedItems.insert(searchIdentifier) @@ -157,7 +157,7 @@ extension SearchableLock: CoreSpotlightSearchable { public static var searchDomain: String { return "com.colemancda.Lock.LockCache" } public var searchIdentifier: String { - return type(of: self).searchIdentifier(for: identifier) + return type(of: self).searchIdentifier(for: id) } public static func searchIdentifier(for lock: UUID) -> String { @@ -165,7 +165,7 @@ extension SearchableLock: CoreSpotlightSearchable { } public var appActivity: AppActivity.ViewData { - return .lock(identifier) + return .lock(id) } public func searchableAttributeSet() -> CSSearchableItemAttributeSet { diff --git a/iOS/LockKit/Model/DeviceManager.swift b/iOS/LockKit/Model/DeviceManager.swift index 70dce01f..a3b15273 100644 --- a/iOS/LockKit/Model/DeviceManager.swift +++ b/iOS/LockKit/Model/DeviceManager.swift @@ -11,29 +11,9 @@ import Bluetooth import GATT import CoreLock -public typealias LockManager = CoreLock.LockManager - -public extension LockManager where Central == NativeCentral { - - static var shared: LockManager { - return LockManagerCache.manager - } -} - #if canImport(CoreBluetooth) && canImport(DarwinGATT) - import CoreBluetooth import DarwinGATT public typealias NativeCentral = DarwinCentral - -private struct LockManagerCache { - - static let options = DarwinCentral.Options(showPowerAlert: false, restoreIdentifier: nil) - static let central = DarwinCentral(options: options) - static let manager = LockManager(central: central) -} - #endif - - diff --git a/iOS/LockKit/Model/FileManager.swift b/iOS/LockKit/Model/FileManager.swift index 62223114..5597fb97 100644 --- a/iOS/LockKit/Model/FileManager.swift +++ b/iOS/LockKit/Model/FileManager.swift @@ -79,7 +79,7 @@ public extension FileManager.Lock { @discardableResult func save(invitation: NewKey.Invitation) throws -> URL { - let fileName = "newKey-\(invitation.key.identifier).ekey" + let fileName = "newKey-\(invitation.key.id).ekey" let data = try jsonEncoder.encode(invitation) let fileURL = documentURL.appendingPathComponent(fileName) diff --git a/iOS/LockKit/Model/Intent.swift b/iOS/LockKit/Model/Intent.swift index 4008670d..1a57c93b 100644 --- a/iOS/LockKit/Model/Intent.swift +++ b/iOS/LockKit/Model/Intent.swift @@ -20,13 +20,13 @@ public extension UnlockIntent { convenience init(lock id: UUID, name: String) { self.init() - self.lock = IntentLock(identifier: identifier, name: name) + self.lock = IntentLock(id: id, name: name) } convenience init(id: UUID, cache: LockCache) { self.init() - self.lock = IntentLock(identifier: identifier, name: cache.name) + self.lock = IntentLock(id: id, name: cache.name) #if os(iOS) && !targetEnvironment(macCatalyst) //self.setImage(INImage(uiImage: UIImage(permission: cache.key.permission)), forParameterNamed: \.lock) @@ -39,7 +39,7 @@ public extension UnlockIntent { public extension IntentLock { convenience init(id: UUID, name: String) { - self.init(identifier: identifier.uuidString, display: name, pronunciationHint: name) + self.init(identifier: id.uuidString, display: name, pronunciationHint: name) } } @@ -53,7 +53,7 @@ public extension INUIAddVoiceShortcutViewController { cache: LockCache, delegate: INUIAddVoiceShortcutViewControllerDelegate) { - let intent = UnlockIntent(identifier: lock, cache: cache) + let intent = UnlockIntent(id: lock, cache: cache) self.init(shortcut: .intent(intent)) self.modalPresentationStyle = .formSheet self.delegate = delegate diff --git a/iOS/LockKit/Model/Log.swift b/iOS/LockKit/Model/Log.swift index d7e85cd9..81e3014d 100644 --- a/iOS/LockKit/Model/Log.swift +++ b/iOS/LockKit/Model/Log.swift @@ -12,6 +12,13 @@ public func log(_ text: String) { let date = Date() +#if DEBUG + DispatchQueue.main.async { + // only print for debug builds + print(text) + } +#endif + DispatchQueue.log.async { // only print for debug builds @@ -71,7 +78,6 @@ public struct Log { } fileprivate init(unsafe url: URL) { - assert(Log(url: url) != nil, "Invalid url \(url)") self.url = url } @@ -81,12 +87,10 @@ public struct Log { } public func load() throws -> String { - return try String(contentsOf: url) } public func log(_ text: String) throws { - let newLine = "\n" + text let data = Data(newLine.utf8) try data.append(fileURL: url) diff --git a/iOS/LockKit/Model/NetService.swift b/iOS/LockKit/Model/NetService.swift index 307e44bd..b1dce0bd 100644 --- a/iOS/LockKit/Model/NetService.swift +++ b/iOS/LockKit/Model/NetService.swift @@ -5,7 +5,7 @@ // Created by Alsey Coleman Miller on 10/16/19. // Copyright © 2019 ColemanCDA. All rights reserved. // - +/* import Foundation import CoreLock import Bonjour @@ -26,3 +26,4 @@ private struct LockNetServiceCache { urlSession: .shared ) } +*/ diff --git a/iOS/LockKit/Model/Queue.swift b/iOS/LockKit/Model/Queue.swift index f3400a17..48f8df9c 100644 --- a/iOS/LockKit/Model/Queue.swift +++ b/iOS/LockKit/Model/Queue.swift @@ -29,6 +29,7 @@ public extension DispatchQueue { } } +@available(*, deprecated, message: "Use Task instead") public extension DispatchQueue { /// Lock App GCD Queue @@ -43,17 +44,6 @@ public extension DispatchQueue { return Cache.queue } - /// Lock Bluetooth operations GCD Queue - static var bluetooth: DispatchQueue { - struct Cache { - static let queue = DispatchQueue( - label: Bundle.Lock.app.rawValue + ".Bluetooth", - qos: .userInitiated - ) - } - return Cache.queue - } - /// Lock Bluetooth operations GCD Queue static var log: DispatchQueue { struct Cache { diff --git a/iOS/LockKit/Model/Shortcut.swift b/iOS/LockKit/Model/Shortcut.swift index 2e3770b7..da6a5ef0 100644 --- a/iOS/LockKit/Model/Shortcut.swift +++ b/iOS/LockKit/Model/Shortcut.swift @@ -24,7 +24,7 @@ public extension INRelevantShortcut { cache: LockCache, location: CLLocationCoordinate2D? = nil) -> INRelevantShortcut { - let intent = UnlockIntent(identifier: lock, cache: cache) + let intent = UnlockIntent(id: lock, cache: cache) let relevantShortcut = INRelevantShortcut(shortcut: .intent(intent)) relevantShortcut.shortcutRole = .action relevantShortcut.relevanceProviders = [ @@ -64,7 +64,7 @@ public extension Store { func setRelevantShortcuts(_ completion: ((Error?) -> Void)? = nil) { - let relevantShortcuts = locks.value.map { (lock, cache) in + let relevantShortcuts = locks.map { (lock, cache) in INRelevantShortcut.unlock(lock: lock, cache: cache) } diff --git a/iOS/LockKit/Model/Store.swift b/iOS/LockKit/Model/Store.swift index c8e5ac17..dde217a9 100644 --- a/iOS/LockKit/Model/Store.swift +++ b/iOS/LockKit/Model/Store.swift @@ -16,14 +16,13 @@ import GATT import DarwinGATT import KeychainAccess import Combine -import OpenCombine -public final class Store { +public final class Store: ObservableObject { public static let shared = Store() private init() { - + // clear keychain on newly installed app. if preferences.isAppInstalled == false { preferences.isAppInstalled = true @@ -66,11 +65,13 @@ public final class Store { #if os(iOS) // observe iBeacons beaconController.beaconChanged = { [unowned self] (beacon) in - switch beacon.state { - case .inside: - self.beaconFound(beacon.uuid) - case .outside: - self.beaconExited(beacon.uuid) + Task { + switch beacon.state { + case .inside: + await self.beaconFound(beacon.uuid) + case .outside: + await self.beaconExited(beacon.uuid) + } } } @@ -79,19 +80,18 @@ public final class Store { #endif // observe local cache changes - locksObserver = locks + locksObserver = $locks .sink(receiveValue: { [weak self] _ in self?.lockCacheChanged() }) // read from filesystem loadCache() } - @available(iOS 13.0, watchOSApplicationExtension 6.0, *) - public lazy var objectWillChange = Combine.ObservableObjectPublisher() - - public let isScanning = OpenCombine.CurrentValueSubject(false) + @Published + public var isScanning = false - public let locks = OpenCombine.CurrentValueSubject<[UUID: LockCache], Never>([UUID: LockCache]()) + @Published + public var locks = [UUID: LockCache]() public lazy var preferences = Preferences(suiteName: .lock)! @@ -111,13 +111,13 @@ public final class Store { } set { fileManager.applicationData = newValue - if locks.value != newValue.locks { - locks.value = newValue.locks // update locks + if locks != newValue.locks { + locks = newValue.locks // update locks } } } - public lazy var lockManager: LockManager = .shared + public lazy var central: NativeCentral = DarwinCentral(options: .init(showPowerAlert: false, restoreIdentifier: nil)) internal lazy var persistentContainer: NSPersistentContainer = .lock @@ -143,25 +143,28 @@ public final class Store { public lazy var spotlight: SpotlightController = .shared - public lazy var netServiceClient: LockNetServiceClient = .shared + //public lazy var netServiceClient: LockNetServiceClient = .shared #endif // BLE cache - public let peripherals = OpenCombine.CurrentValueSubject<[NativeCentral.Peripheral: LockPeripheral], Never>([NativeCentral.Peripheral: LockPeripheral]()) - public let lockInformation = OpenCombine.CurrentValueSubject<[NativeCentral.Peripheral: LockInformationCharacteristic], Never>([NativeCentral.Peripheral: LockInformationCharacteristic]()) + @Published + public var peripherals = [NativeCentral.Peripheral: ScanData]() - private var locksObserver: OpenCombine.AnyCancellable? + @Published + public var lockInformation = [NativeCentral.Peripheral: LockInformation]() + + private var locksObserver: Combine.AnyCancellable? // MARK: - Subscript /// Cached information for the specified lock. public subscript (lock id: UUID) -> LockCache? { - get { return locks.value[identifier] } + get { return locks[id] } set { - locks.value[identifier] = newValue // update observers - applicationData.locks = locks.value // write file + locks[id] = newValue // update observers + applicationData.locks = locks // write file } } @@ -171,7 +174,7 @@ public final class Store { get { do { - guard let data = try keychain.getData(identifier.uuidString) + guard let data = try keychain.getData(id.uuidString) else { return nil } guard let key = KeyData(data: data) else { assertionFailure("Invalid key data"); return nil } @@ -186,7 +189,7 @@ public final class Store { } set { - let key = identifier.uuidString + let key = id.uuidString do { guard let data = newValue?.data else { try keychain.remove(key) @@ -208,7 +211,7 @@ public final class Store { /// The Bluetooth LE peripheral for the speciifed lock. public subscript (peripheral id: UUID) -> NativeCentral.Peripheral? { - return lockInformation.value.first(where: { $0.value.identifier == identifier })?.key + return lockInformation.first(where: { $0.value.id == id })?.key } /// Remove the specified lock from the cache and keychain. @@ -219,7 +222,7 @@ public final class Store { else { return false } self[lock: lock] = nil - self[key: lockCache.key.identifier] = nil + self[key: lockCache.key.id] = nil return true } @@ -227,9 +230,9 @@ public final class Store { /// 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] + let keyData = self[key: cache.key.id] else { return nil } - return .init(identifier: cache.key.identifier, secret: keyData) + return .init(id: cache.key.id, secret: keyData) } /// Forceably load cache. @@ -238,8 +241,8 @@ public final class Store { // read file let applicationData = self.applicationData // set value - if locks.value != applicationData.locks { - locks.value = applicationData.locks + if locks != applicationData.locks { + locks = applicationData.locks } } @@ -262,18 +265,17 @@ public final class Store { internal func updateCoreData() { - let locks = self.locks.value + let locks = self.locks backgroundContext.commit { try $0.insert(locks) } } #if os(iOS) - private func monitorBeacons() { // always monitor lock notification iBeacon - let beacons = Set(self.locks.value.keys) + [.lockNotificationBeacon] + let beacons = Set(self.locks.keys) + [.lockNotificationBeacon] let oldBeacons = self.beaconController.beacons.keys // remove old beacons @@ -294,11 +296,11 @@ public final class Store { private func updateSpotlight() { guard SpotlightController.isSupported else { return } - spotlight.reindexAll(locks: locks.value) + spotlight.reindexAll(locks: locks) } private func updateCloud() { - + /* DispatchQueue.cloud.async { [weak self] in guard let self = self else { return } do { @@ -306,28 +308,26 @@ public final class Store { try self.syncCloud() } catch { log("⚠️ Unable to sync iCloud: \(error.localizedDescription)") } - } + }*/ } - private func beaconFound(_ beacon: UUID) { + @MainActor + private func beaconFound(_ beacon: UUID) async { // Can't do anything because we don't have Bluetooth - guard lockManager.central.state == .poweredOn + guard await central.state == .poweredOn else { return } if let _ = Store.shared[lock: beacon] { - DispatchQueue.bluetooth.async { [weak self] in - guard let self = self else { return } - do { - guard let _ = try self.device(for: beacon, scanDuration: 1.0) else { - log("⚠️ Could not find lock \(beacon) for beacon \(beacon)") - self.beaconController.scanBeacon(for: beacon) - return - } - log("📶 Found lock \(beacon)") - } catch { - log("⚠️ Could not scan: \(error.localizedDescription)") + do { + guard let _ = try await self.device(for: beacon, scanDuration: 1.0) else { + log("⚠️ Could not find lock \(beacon) for beacon \(beacon)") + self.beaconController.scanBeacon(for: beacon) + return } + log("📶 Found lock \(beacon)") + } catch { + log("⚠️ Could not scan: \(error.localizedDescription)") } } else if beacon == .lockNotificationBeacon { // Entered region event log("📶 Lock notification") @@ -336,226 +336,221 @@ public final class Store { typealias FetchRequest = LockEvent.FetchRequest typealias Predicate = LockEvent.Predicate let context = Store.shared.backgroundContext - DispatchQueue.bluetooth.async { - // scan for all locks - let locks = Store.shared.locks.value.keys - // scan if none is visible - if locks.compactMap({ self.device(for: $0) }).isEmpty { - do { try Store.shared.scan(duration: 1.0) } - catch { log("⚠️ Could not scan for locks: \(error.localizedDescription)") } - } - let visibleLocks = locks.filter { self.device(for: $0) != nil } - // queue fetching events - DispatchQueue.bluetooth.asyncAfter(deadline: .now() + 3.0) { - defer { self.beaconController.scanBeacons() } // refresh beacons - for lock in visibleLocks { - do { - guard let device = try self.device(for: lock, scanDuration: 1.0) - else { continue } - 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 - ) - ) - try self.listEvents(device, fetchRequest: fetchRequest, notification: { _,_ in }) - } catch { - log("⚠️ Could not fetch latest data for lock \(lock): \(error.localizedDescription)") - continue - } + + // scan for all locks + let locks = Store.shared.locks.keys + // scan if none is visible + if locks.compactMap({ self.device(for: $0) }).isEmpty { + do { try await Store.shared.scan(duration: 1.0) } + catch { log("⚠️ Could not scan for locks: \(error.localizedDescription)") } + } + let visibleLocks = locks.filter { self.device(for: $0) != nil } + // queue fetching events + try? await Task.sleep(nanoseconds: 3 * 1_000_000_000) + defer { self.beaconController.scanBeacons() } // refresh beacons + for lock in visibleLocks { + do { + guard let device = try await self.device(for: lock, scanDuration: 1.0) + else { continue } + let lastEventDate = try context.performErrorBlockAndWait { + try context.find(id: 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 + ) + ) + try await self.listEvents(device, fetchRequest: fetchRequest, notification: { _,_ in }) + } catch { + log("⚠️ Could not fetch latest data for lock \(lock): \(error.localizedDescription)") + continue } } } } - private func beaconExited(_ beacon: UUID) { + @MainActor + private func beaconExited(_ beacon: UUID) async { guard let _ = Store.shared[lock: beacon] else { return } - DispatchQueue.bluetooth.async { [weak self] in - guard let self = self else { return } - do { - try self.scan(duration: 1.0) - if self.device(for: beacon) == nil { - log("Lock \(beacon) no longer in range") - } else { - // lock is in range, refresh beacons - self.beaconController.scanBeacon(for: beacon) - } - } catch { - log("⚠️ Could not scan: \(error.localizedDescription)") + do { + try await self.scan(duration: 1.0) + if self.device(for: beacon) == nil { + log("Lock \(beacon) no longer in range") + } else { + // lock is in range, refresh beacons + self.beaconController.scanBeacon(for: beacon) } + } catch { + log("⚠️ Could not scan: \(error.localizedDescription)") } } - #endif } -// MARK: - ObservableObject - -extension Store: Combine.ObservableObject { } - // MARK: - Lock Bluetooth Operations +@MainActor public extension Store { func device(for id: UUID, - scanDuration: TimeInterval) throws -> LockPeripheral? { - - assert(Thread.isMainThread == false) - - if let lock = device(for: identifier) { + scanDuration: TimeInterval) async throws -> NativeCentral.Peripheral? { + + if let lock = device(for: id) { return lock } else { - try self.scan(duration: scanDuration) - for peripheral in peripherals.value.values { + try await self.scan(duration: scanDuration) + for peripheral in peripherals.keys { // skip known locks that are not the targeted device - if let information = lockInformation.value[peripheral.scanData.peripheral] { - guard information.identifier == identifier else { continue } + if let information = lockInformation[peripheral] { + guard information.id == id else { continue } } // request information do { - try self.readInformation(peripheral) + try await self.readInformation(peripheral) } catch { log("⚠️ Could not read information: \(error.localizedDescription)") continue // ignore } - if let foundDevice = device(for: identifier) { + if let foundDevice = device(for: id) { return foundDevice } } - return device(for: identifier) + return device(for: id) } } - func device(for id: UUID) -> LockPeripheral? { - - guard let peripheral = self[peripheral: identifier], - let lock = self.peripherals.value[peripheral] - else { return nil } - - return lock + func device(for id: UUID) -> NativeCentral.Peripheral? { + self[peripheral: id] } - func key(for lock: LockPeripheral) -> KeyCredentials? { + func key(for lock: NativeCentral.Peripheral) -> KeyCredentials? { - guard let information = self.lockInformation.value[lock.scanData.peripheral], - let lockCache = self[lock: information.identifier], - let keyData = self[key: lockCache.key.identifier] + guard let information = self.lockInformation[lock], + let lockCache = self[lock: information.id], + let keyData = self[key: lockCache.key.id] else { return nil } let key = KeyCredentials( - identifier: lockCache.key.identifier, + id: lockCache.key.id, secret: keyData ) return key } - func scan(duration: TimeInterval? = nil) throws { + func scan(duration: TimeInterval? = nil) async throws { let duration = duration ?? preferences.scanDuration let filterDuplicates = preferences.filterDuplicates - assert(Thread.isMainThread == false) - self.peripherals.value.removeAll() - try lockManager.scanLocks(duration: duration, filterDuplicates: filterDuplicates) { [unowned self] in - self.peripherals.value[$0.scanData.peripheral] = $0 + self.peripherals.removeAll() + let stream = central.scan(with: [LockService.uuid]) + for try await scanData in stream { + guard let serviceUUIDs = scanData.advertisementData.serviceUUIDs, + serviceUUIDs.contains(LockService.uuid) + else { continue } + // cache found device + self.peripherals[scanData.peripheral] = scanData } + //try lockManager.scanLocks(duration: duration, filterDuplicates: filterDuplicates) { [unowned self] in + // self.peripherals.value[$0.scanData.peripheral] = $0 + //} } /// Setup a lock. - func setup(_ lock: LockPeripheral, - sharedSecret: KeyData, - name: String) throws { + func setup( + _ lock: NativeCentral.Peripheral, + sharedSecret: KeyData, + name: String + ) async throws { - assert(Thread.isMainThread == false) let setupRequest = SetupRequest() - let information = try lockManager.setup( + let information = try await central.setup( setupRequest, - for: lock.scanData.peripheral, - sharedSecret: sharedSecret, - timeout: preferences.bluetoothTimeout + using: sharedSecret, + for: lock ) let ownerKey = Key(setup: setupRequest) let lockCache = LockCache( key: ownerKey, name: name, - information: .init(characteristic: information) + information: .init(information) ) // store key - self[lock: information.identifier] = lockCache - self[key: ownerKey.identifier] = setupRequest.secret + self[lock: information.id] = lockCache + self[key: ownerKey.id] = setupRequest.secret // update lock information - self.lockInformation.value[lock.scanData.peripheral] = information + self.lockInformation[lock] = information } - func readInformation(_ lock: LockPeripheral) throws { + func readInformation(_ lock: NativeCentral.Peripheral) async throws { assert(Thread.isMainThread == false) - let information = try lockManager.readInformation( - for: lock.scanData.peripheral, - timeout: preferences.bluetoothTimeout + let information = try await central.readInformation( + for: lock ) // update lock information cache - self.lockInformation.value[lock.scanData.peripheral] = information - self[lock: information.identifier]?.information = LockCache.Information(characteristic: information) + self.lockInformation[lock] = information + self[lock: information.id]?.information = LockCache.Information(information) } @discardableResult - func unlock(_ lock: LockPeripheral, action: UnlockAction = .default) throws -> Bool { + func unlock(_ lock: NativeCentral.Peripheral, action: UnlockAction = .default) async throws -> Bool { // get lock key guard let key = self.key(for: lock) else { return false } - try lockManager.unlock(action, - for: lock.scanData.peripheral, - with: key, - timeout: preferences.bluetoothTimeout) + try await central.unlock( + action, + using: key, + for: lock + ) + return true } @discardableResult - func listKeys(_ lock: LockPeripheral, - notification: @escaping ((KeysList, Bool) -> ()) = { _,_ in }) throws -> Bool { + func listKeys( + _ lock: NativeCentral.Peripheral, + notification updateBlock: ((KeysList, Bool) -> ()) = { _,_ in } + ) async throws -> Bool { // get lock key - guard let information = self.lockInformation.value[lock.scanData.peripheral], - let lockCache = self[lock: information.identifier], - let keyData = self[key: lockCache.key.identifier] + guard let information = self.lockInformation[lock], + let lockCache = self[lock: information.id], + let keyData = self[key: lockCache.key.id] else { return false } let key = KeyCredentials( - identifier: lockCache.key.identifier, + id: lockCache.key.id, secret: keyData ) + + let context = backgroundContext // BLE request - try lockManager.listKeys(for: lock.scanData.peripheral, with: key, timeout: preferences.bluetoothTimeout) { [weak self] (list, isComplete) in - // call completion block - notification(list, isComplete) - // store in CoreData - guard list.keys.isEmpty == false else { return } - self?.backgroundContext.commit { (context) in - try list.keys.forEach { - try context.insert($0, for: information.identifier) - } - try list.newKeys.forEach { - try context.insert($0, for: information.identifier) + try await central.connection(for: lock) { + let stream = try await $0.listKeys(using: key, log: { log("📲 Central: " + $0) }) + var list = KeysList() + for try await notification in stream { + list.append(notification.key) + // call completion block + updateBlock(list, notification.isLast) + await context.commit { (context) in + try context.insert(notification.key, for: information.id) } } } @@ -569,35 +564,46 @@ public extension Store { } @discardableResult - func listEvents(_ lock: LockPeripheral, - fetchRequest: LockEvent.FetchRequest? = nil, - notification: @escaping ((EventsList, Bool) -> ()) = { _,_ in }) throws -> Bool { + func listEvents( + _ lock: NativeCentral.Peripheral, + fetchRequest: LockEvent.FetchRequest? = nil, + notification updateBlock: @escaping ((EventsList, Bool) -> ()) = { _,_ in } + ) async throws -> Bool { // get lock key - guard let information = self.lockInformation.value[lock.scanData.peripheral], - let lockCache = self[lock: information.identifier], - let keyData = self[key: lockCache.key.identifier] + guard let information = self.lockInformation[lock], + let lockCache = self[lock: information.id], + let keyData = self[key: lockCache.key.id] else { return false } let key = KeyCredentials( - identifier: lockCache.key.identifier, + id: lockCache.key.id, secret: keyData ) - let lockIdentifier = information.identifier + let lockIdentifier = information.id + let context = backgroundContext // BLE request - var events = [LockEvent]() - try lockManager.listEvents(fetchRequest: fetchRequest, for: lock.scanData.peripheral, with: key, timeout: preferences.bluetoothTimeout) { [weak self] (list, isComplete) in - // call completion block - notification(list, isComplete) - events = list - // store in CoreData - self?.backgroundContext.commit { (context) in - try context.insert(list, for: information.identifier) + let log = central.log + try await central.connection(for: lock) { + let stream = try await $0.listEvents(fetchRequest: fetchRequest, using: key, log: log) + var events = [LockEvent]() + for try await notification in stream { + if let event = notification.event { + events.append(event) + log?("Recieved event \(event.id)") + await context.commit { (context) in + try context.insert(event, for: information.id) + } + } + // call completion block + updateBlock(events, notification.isLast) + } } + /* #if os(iOS) if preferences.isCloudBackupEnabled { DispatchQueue.cloud.async { [weak self] in @@ -613,11 +619,11 @@ public extension Store { } } #endif - + */ return true } } - +/* // MARK: - Bonjour Requests #if os(iOS) @@ -629,12 +635,12 @@ public extension Store { fetchRequest: LockEvent.FetchRequest? = nil) throws -> Bool { // get lock key - guard let lockCache = self[lock: lock.identifier], - let keyData = self[key: lockCache.key.identifier] + guard let lockCache = self[lock: lock.id], + let keyData = self[key: lockCache.key.id] else { return false } let key = KeyCredentials( - identifier: lockCache.key.identifier, + id: lockCache.key.id, secret: keyData ) @@ -646,7 +652,7 @@ public extension Store { ) backgroundContext.commit { (context) in - try context.insert(events, for: lock.identifier) + try context.insert(events, for: lock.id) } #if os(iOS) @@ -655,7 +661,7 @@ public extension Store { // upload to iCloud do { for event in events { - let value = LockEvent.Cloud(event: event, for: lock.identifier) + let value = LockEvent.Cloud(event: event, for: lock.id) try self?.cloud.upload(value) } } catch { @@ -670,7 +676,7 @@ public extension Store { } #endif - +*/ // MARK: - CloudKit Operations #if os(iOS) diff --git a/iOS/LockKit/Model/UserActivity.swift b/iOS/LockKit/Model/UserActivity.swift index 63b8420d..a61b2fbe 100644 --- a/iOS/LockKit/Model/UserActivity.swift +++ b/iOS/LockKit/Model/UserActivity.swift @@ -237,7 +237,7 @@ public extension NSUserActivity { if let lockCache = Store.shared[lock: lockIdentifier] { self.title = lockCache.name #if os(iOS) - self.contentAttributeSet = SearchableLock(identifier: lockIdentifier, cache: lockCache).searchableAttributeSet() + self.contentAttributeSet = SearchableLock(id: lockIdentifier, cache: lockCache).searchableAttributeSet() #endif } else { self.title = "Lock \(lockIdentifier)" diff --git a/iOS/LockKit/Model/iCloud/CloudApplicationData.swift b/iOS/LockKit/Model/iCloud/CloudApplicationData.swift index 0e9db1a2..96514bfc 100644 --- a/iOS/LockKit/Model/iCloud/CloudApplicationData.swift +++ b/iOS/LockKit/Model/iCloud/CloudApplicationData.swift @@ -25,12 +25,12 @@ public extension ApplicationData.Cloud { init(_ value: ApplicationData, user: CloudUser.ID) { - self.id = .init(rawValue: value.identifier) + self.id = .init(rawValue: value.id) self.created = value.created self.updated = value.updated self.locks = value.locks .sorted(by: { $0.key.uuidString > $1.key.uuidString }) - .map { LockCache.Cloud(lock: $0.key, cache: $0.value, applicationData: value.identifier) } + .map { LockCache.Cloud(lock: $0.key, cache: $0.value, applicationData: value.id) } } } @@ -44,7 +44,7 @@ public extension ApplicationData { locks[lock.id.rawValue] = value } self.init( - identifier: cloud.id.rawValue, + id: cloud.id.rawValue, created: cloud.created, updated: cloud.updated, locks: locks diff --git a/iOS/LockKit/Model/iCloud/CloudEvent.swift b/iOS/LockKit/Model/iCloud/CloudEvent.swift index 7f4a2ea2..c3b52ae6 100644 --- a/iOS/LockKit/Model/iCloud/CloudEvent.swift +++ b/iOS/LockKit/Model/iCloud/CloudEvent.swift @@ -43,7 +43,7 @@ internal extension LockEvent.Cloud { switch event { case let .setup(event): self.type = .setup - self.id = .init(rawValue: event.identifier) + self.id = .init(rawValue: event.id) self.date = event.date self.key = event.key self.newKey = nil @@ -52,7 +52,7 @@ internal extension LockEvent.Cloud { self.unlockAction = nil case let .unlock(event): self.type = .unlock - self.id = .init(rawValue: event.identifier) + self.id = .init(rawValue: event.id) self.date = event.date self.key = event.key self.unlockAction = event.action @@ -61,7 +61,7 @@ internal extension LockEvent.Cloud { self.removedKeyType = nil case let .createNewKey(event): self.type = .createNewKey - self.id = .init(rawValue: event.identifier) + self.id = .init(rawValue: event.id) self.date = event.date self.key = event.key self.newKey = event.newKey @@ -70,7 +70,7 @@ internal extension LockEvent.Cloud { self.unlockAction = nil case let .confirmNewKey(event): self.type = .confirmNewKey - self.id = .init(rawValue: event.identifier) + self.id = .init(rawValue: event.id) self.date = event.date self.key = event.key self.newKey = event.newKey @@ -79,7 +79,7 @@ internal extension LockEvent.Cloud { self.unlockAction = nil case let .removeKey(event): self.type = .removeKey - self.id = .init(rawValue: event.identifier) + self.id = .init(rawValue: event.id) self.date = event.date self.key = event.key self.removedKey = event.removedKey @@ -95,24 +95,24 @@ internal extension LockEvent { init?(_ cloud: LockEvent.Cloud) { switch cloud.type { case .setup: - self = .setup(.init(identifier: cloud.id.rawValue, date: cloud.date, key: cloud.key)) + self = .setup(.init(id: cloud.id.rawValue, date: cloud.date, key: cloud.key)) case .unlock: guard let action = cloud.unlockAction else { return nil } - self = .unlock(.init(identifier: cloud.id.rawValue, date: cloud.date, key: cloud.key, action: action)) + self = .unlock(.init(id: cloud.id.rawValue, date: cloud.date, key: cloud.key, action: action)) case .createNewKey: guard let newKey = cloud.newKey else { return nil } - self = .createNewKey(.init(identifier: cloud.id.rawValue, date: cloud.date, key: cloud.key, newKey: newKey)) + self = .createNewKey(.init(id: cloud.id.rawValue, date: cloud.date, key: cloud.key, newKey: newKey)) case .confirmNewKey: guard let newKey = cloud.newKey else { return nil } - self = .confirmNewKey(.init(identifier: cloud.id.rawValue, date: cloud.date, newKey: newKey, key: cloud.key)) + self = .confirmNewKey(.init(id: cloud.id.rawValue, date: cloud.date, newKey: newKey, key: cloud.key)) case .removeKey: guard let removedKey = cloud.removedKey, let removedKeyType = cloud.removedKeyType else { return nil } - self = .removeKey(.init(identifier: cloud.id.rawValue, date: cloud.date, key: cloud.key, removedKey: removedKey, type: removedKeyType)) + self = .removeKey(.init(id: cloud.id.rawValue, date: cloud.date, key: cloud.key, removedKey: removedKey, type: removedKeyType)) } } } diff --git a/iOS/LockKit/Model/iCloud/CloudKey.swift b/iOS/LockKit/Model/iCloud/CloudKey.swift index fbc8dc45..c93bb4c9 100644 --- a/iOS/LockKit/Model/iCloud/CloudKey.swift +++ b/iOS/LockKit/Model/iCloud/CloudKey.swift @@ -38,13 +38,13 @@ public extension Key { public extension Key.Cloud { init(_ value: Key, lock: UUID) { - self.id = .init(rawValue: value.identifier) + self.id = .init(rawValue: value.id) self.lock = .init(rawValue: lock) self.name = value.name self.created = value.created self.permissionType = value.permission.type if case let .scheduled(schedule) = value.permission { - self.schedule = Permission.Schedule.Cloud(schedule, key: value.identifier, type: .key) + self.schedule = Permission.Schedule.Cloud(schedule, key: value.id, type: .key) } else { self.schedule = nil } @@ -73,7 +73,7 @@ public extension Key { } self.init( - identifier: id, + id: id, name: cloud.name, created: cloud.created, permission: permission diff --git a/iOS/LockKit/Model/iCloud/CloudNewKey.swift b/iOS/LockKit/Model/iCloud/CloudNewKey.swift index 3d2487ae..dcd30fcf 100644 --- a/iOS/LockKit/Model/iCloud/CloudNewKey.swift +++ b/iOS/LockKit/Model/iCloud/CloudNewKey.swift @@ -35,14 +35,14 @@ public extension NewKey { public extension NewKey.Cloud { init(_ value: NewKey, lock: UUID) { - self.id = .init(rawValue: value.identifier) + self.id = .init(rawValue: value.id) self.lock = .init(rawValue: lock) self.name = value.name self.created = value.created self.expiration = value.expiration self.permissionType = value.permission.type if case let .scheduled(schedule) = value.permission { - self.schedule = Permission.Schedule.Cloud(schedule, key: value.identifier, type: .newKey) + self.schedule = Permission.Schedule.Cloud(schedule, key: value.id, type: .newKey) } else { self.schedule = nil } @@ -71,7 +71,7 @@ public extension NewKey { } self.init( - identifier: id, + id: id, name: cloud.name, permission: permission, created: cloud.created, diff --git a/iOS/LockKit/Model/iCloud/CloudNewKeyInvitation.swift b/iOS/LockKit/Model/iCloud/CloudNewKeyInvitation.swift index 4b2194ab..f59ef8b1 100644 --- a/iOS/LockKit/Model/iCloud/CloudNewKeyInvitation.swift +++ b/iOS/LockKit/Model/iCloud/CloudNewKeyInvitation.swift @@ -39,7 +39,7 @@ internal extension NewKey.Invitation.Cloud { init(_ value: NewKey.Invitation) { - self.id = .init(rawValue: value.key.identifier) + self.id = .init(rawValue: value.key.id) self.lock = value.lock self.secret = value.secret self.key = try! NewKey.Invitation.Cloud.keyEncoder.encode(value.key) diff --git a/iOS/LockKit/Model/iCloud/CloudShare.swift b/iOS/LockKit/Model/iCloud/CloudShare.swift index 0e68d4bc..74134ad1 100644 --- a/iOS/LockKit/Model/iCloud/CloudShare.swift +++ b/iOS/LockKit/Model/iCloud/CloudShare.swift @@ -171,7 +171,7 @@ public extension CloudStore { // upload public share data with invitation url let publicShare = CloudShare.NewKey( - id: .init(rawValue: invitation.key.identifier), + id: .init(rawValue: invitation.key.id), invitation: shareURL, user: user.cloudRecordID.recordName ) diff --git a/iOS/LockKit/Model/iCloud/iCloud.swift b/iOS/LockKit/Model/iCloud/iCloud.swift index 8ab6e8c4..eada8452 100644 --- a/iOS/LockKit/Model/iCloud/iCloud.swift +++ b/iOS/LockKit/Model/iCloud/iCloud.swift @@ -176,10 +176,10 @@ public final class CloudStore { // download keys from keychain var keys = [UUID: KeyData](minimumCapacity: applicationData.locks.count) for key in applicationData.keys { - guard let data = try keychain.getData(key.identifier.uuidString), + guard let data = try keychain.getData(key.id.uuidString), let keyData = KeyData(data: data) - else { throw Error.missingKeychainItem(key.identifier) } - keys[key.identifier] = keyData + else { throw Error.missingKeychainItem(key.id) } + keys[key.id] = keyData } return (applicationData, keys) @@ -256,7 +256,7 @@ internal extension ApplicationData { func update(with applicationData: ApplicationData) -> ApplicationData? { // must be originally the same application data - guard self.identifier == applicationData.identifier, + guard self.id == applicationData.id, self.created == applicationData.created else { return nil } @@ -353,7 +353,7 @@ public extension Store { } func downloadCloudLocks() throws { - + /* var insertedEventsCount = 0 var insertedKeysCount = 0 var insertedNewKeysCount = 0 @@ -391,7 +391,7 @@ public extension Store { return true // more events, ignore invalid cloud value } // if already in CoreData, then stop - guard try EventManagedObject.find(event.identifier, in: context) == nil + guard try EventManagedObject.find(event.id, in: context) == nil else { return false } // save event in CoreData context.commit { @@ -408,7 +408,7 @@ public extension Store { return true // more values, ignore invalid cloud value } // if already in CoreData, then stop - guard try context.find(identifier: value.identifier, type: KeyManagedObject.self) == nil + guard try context.find(identifier: value.id, type: KeyManagedObject.self) == nil else { return false } // save value in CoreData context.commit { @@ -425,7 +425,7 @@ public extension Store { return true // more values, ignore invalid cloud value } // if already in CoreData, then stop - guard try context.find(identifier: value.identifier, type: NewKeyManagedObject.self) == nil + guard try context.find(identifier: value.id, type: NewKeyManagedObject.self) == nil else { return false } // save value in CoreData context.commit { @@ -436,7 +436,7 @@ public extension Store { } return true - } + }*/ } @discardableResult @@ -472,9 +472,9 @@ public extension Store { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .short dateFormatter.timeStyle = .medium - print("Cloud: \(cloudData.identifier) \(dateFormatter.string(from: cloudData.updated))") + print("Cloud: \(cloudData.id) \(dateFormatter.string(from: cloudData.updated))") dump(cloudData) - print("Local: \(oldApplicationData.identifier) \(dateFormatter.string(from: oldApplicationData.updated))") + print("Local: \(oldApplicationData.id) \(dateFormatter.string(from: oldApplicationData.updated))") dump(oldApplicationData) #endif @@ -502,8 +502,8 @@ public extension Store { // remove old keys var removedKeys = 0 let newData = self.applicationData - let newKeys = newData.keys.map { $0.identifier } - let oldKeys = oldApplicationData.keys.map { $0.identifier } + let newKeys = newData.keys.map { $0.id } + let oldKeys = oldApplicationData.keys.map { $0.id } for oldKey in oldKeys { // old key no longer exists guard newKeys.contains(oldKey) == false @@ -526,8 +526,8 @@ public extension Store { // read from to keychain var keys = [UUID: KeyData]() for key in applicationData.keys { - let keyData = self[key: key.identifier] - keys[key.identifier] = keyData + let keyData = self[key: key.id] + keys[key.id] = keyData } // upload keychain and application data to iCloud @@ -543,7 +543,7 @@ import UIKit public extension ActivityIndicatorViewController where Self: UIViewController { func syncCloud(showActivity: Bool) { - + /* assert(Thread.isMainThread) guard Store.shared.preferences.isCloudBackupEnabled else { return } @@ -552,7 +552,7 @@ public extension ActivityIndicatorViewController where Self: UIViewController { try Store.shared.syncCloud(conflicts: { self?.resolveCloudSyncConflicts($0) }) }, completion: { (viewController, _) in - }) + })*/ } } diff --git a/iOS/Message/MessagesViewController.swift b/iOS/Message/MessagesViewController.swift index d317e839..bcb32360 100644 --- a/iOS/Message/MessagesViewController.swift +++ b/iOS/Message/MessagesViewController.swift @@ -193,10 +193,10 @@ final class MessagesViewController: MSMessagesAppViewController { private func select(_ item: Item) { - log("Selected \(item.cache.name) \(item.identifier)") + log("Selected \(item.cache.name) \(item.id)") requestPresentationStyle(.expanded) - shareKey(lock: item.identifier) { [unowned self] in + shareKey(lock: item.id) { [unowned self] in self.dismiss(animated: true, completion: nil) self.requestPresentationStyle(.compact) guard let invitation = $0?.invitation else { diff --git a/iOS/SmartLock.xcodeproj/project.pbxproj b/iOS/SmartLock.xcodeproj/project.pbxproj index daad1a28..93f65705 100644 --- a/iOS/SmartLock.xcodeproj/project.pbxproj +++ b/iOS/SmartLock.xcodeproj/project.pbxproj @@ -72,15 +72,13 @@ 6E50FC15233B63D5003F3AF7 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 6E50FC19233B63D5003F3AF7 /* Intents.intentdefinition */; }; 6E50FC16233B63D5003F3AF7 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 6E50FC19233B63D5003F3AF7 /* Intents.intentdefinition */; settings = {ATTRIBUTES = (no_codegen, ); }; }; 6E50FC17233B63D5003F3AF7 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 6E50FC19233B63D5003F3AF7 /* Intents.intentdefinition */; settings = {ATTRIBUTES = (no_codegen, ); }; }; - 6E5DF8B32319E3A50070787B /* OpenCombine in Frameworks */ = {isa = PBXBuildFile; productRef = 6E5DF8B22319E3A50070787B /* OpenCombine */; }; - 6E5DF8C32319EB020070787B /* OpenCombine in Frameworks */ = {isa = PBXBuildFile; productRef = 6E5DF8C22319EB020070787B /* OpenCombine */; }; 6E6000142121119400787DAA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6000132121119400787DAA /* AppDelegate.swift */; }; 6E6000192121119400787DAA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6E6000172121119400787DAA /* Main.storyboard */; }; 6E60001E2121119500787DAA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6E60001C2121119500787DAA /* LaunchScreen.storyboard */; }; 6E6000292121119500787DAA /* SmartLockUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6000282121119500787DAA /* SmartLockUITests.swift */; }; 6E64B7EA231236E5002BC0A2 /* Rswift in Frameworks */ = {isa = PBXBuildFile; productRef = 6E64B7E9231236E5002BC0A2 /* Rswift */; }; 6E64B7ED23123846002BC0A2 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 6E64B7EC23123846002BC0A2 /* KeychainAccess */; }; - 6E64B7F1231240CC002BC0A2 /* JGProgressHUD in Frameworks */ = {isa = PBXBuildFile; platformFilter = maccatalyst; productRef = 6E64B7F0231240CC002BC0A2 /* JGProgressHUD */; }; + 6E64B7F1231240CC002BC0A2 /* JGProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = 6E64B7F0231240CC002BC0A2 /* JGProgressHUD */; }; 6E64B7F4231247EF002BC0A2 /* QRCodeReader in Frameworks */ = {isa = PBXBuildFile; platformFilter = ios; productRef = 6E64B7F3231247EF002BC0A2 /* QRCodeReader */; }; 6E6B78B62325F14600635EEF /* SliderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E135CBD2319842A00FFA7F7 /* SliderCell.swift */; }; 6E6B78B72325F17300635EEF /* BluetoothSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E135CB72319832900FFA7F7 /* BluetoothSettingsView.swift */; }; @@ -226,8 +224,6 @@ 6E83BB48233D906C00D6BA04 /* LockDetail.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 36FC4C94233A249500A5D5B3 /* LockDetail.storyboard */; }; 6E83BB49233D906C00D6BA04 /* Logs.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 36FC4C75233A17AE00A5D5B3 /* Logs.storyboard */; }; 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 */; }; @@ -423,13 +419,6 @@ remoteGlobalIDString = DD75027A1C68FCFC006590AF; remoteInfo = CoreLockTests; }; - 6E83BACD233D906C00D6BA04 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 6E8772BB231258CC003B9469 /* JGProgressHUD.xcodeproj */; - proxyType = 1; - remoteGlobalIDString = 6BF7DC591AEFCFD9000A0AE0; - remoteInfo = "JGProgressHUD-iOS"; - }; 6E83BACF233D906C00D6BA04 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 6E60004D2121164B00787DAA /* CoreLock.xcodeproj */; @@ -437,20 +426,6 @@ remoteGlobalIDString = 52D6D97B1BEFF229002C0205; remoteInfo = "CoreLock-iOS"; }; - 6E8772C4231258CD003B9469 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 6E8772BB231258CC003B9469 /* JGProgressHUD.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 6BF7DC5A1AEFCFD9000A0AE0; - remoteInfo = "JGProgressHUD-iOS"; - }; - 6E8772C6231258CD003B9469 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 6E8772BB231258CC003B9469 /* JGProgressHUD.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 6BABF9A31F7A6133005E783F; - remoteInfo = "JGProgressHUD-tvOS"; - }; 6E8772CC23125A70003B9469 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 6E60004D2121164B00787DAA /* CoreLock.xcodeproj */; @@ -458,13 +433,6 @@ remoteGlobalIDString = 52D6D97B1BEFF229002C0205; remoteInfo = "CoreLock-iOS"; }; - 6E8772D0231265B4003B9469 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 6E8772BB231258CC003B9469 /* JGProgressHUD.xcodeproj */; - proxyType = 1; - remoteGlobalIDString = 6BF7DC591AEFCFD9000A0AE0; - remoteInfo = "JGProgressHUD-iOS"; - }; 6E994A8E230DFB6D0076DC1B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 6E6000082121119400787DAA /* Project object */; @@ -605,13 +573,6 @@ remoteGlobalIDString = 6E994A83230DFB6C0076DC1B; remoteInfo = LockKit; }; - 6ECDD09A234D8B3400178B25 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 6E8772BB231258CC003B9469 /* JGProgressHUD.xcodeproj */; - proxyType = 1; - remoteGlobalIDString = 6BF7DC591AEFCFD9000A0AE0; - remoteInfo = "JGProgressHUD-iOS"; - }; 6ED820952316774E00B69520 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 6E60004D2121164B00787DAA /* CoreLock.xcodeproj */; @@ -637,7 +598,6 @@ files = ( 6E9C95FD23114D07007C18FE /* CoreLock.framework in Embed Frameworks */, 6E9C95FF23114D60007C18FE /* LockKit.framework in Embed Frameworks */, - 6E8772CB231259DE003B9469 /* JGProgressHUD.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -837,7 +797,6 @@ 6E83BAF52323842500FBC12E /* TableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = ""; }; 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; }; @@ -973,7 +932,6 @@ files = ( 6E9C95FC23114D07007C18FE /* CoreLock.framework in Frameworks */, 6E9C95FE23114D60007C18FE /* LockKit.framework in Frameworks */, - 6E8772CA231259DE003B9469 /* JGProgressHUD.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1005,7 +963,6 @@ 6E64B7F4231247EF002BC0A2 /* QRCodeReader in Frameworks */, 6E64B7F1231240CC002BC0A2 /* JGProgressHUD in Frameworks */, 6ED44E44235C4406002E8F54 /* SFSafeSymbols in Frameworks */, - 6E5DF8B32319E3A50070787B /* OpenCombine in Frameworks */, 6EE5A10B2329FCA00000235C /* CloudKitCodable in Frameworks */, 6E64B7ED23123846002BC0A2 /* KeychainAccess in Frameworks */, ); @@ -1101,7 +1058,6 @@ buildActionMask = 2147483647; files = ( 6ED8209A2316777A00B69520 /* KeychainAccess in Frameworks */, - 6E5DF8C32319EB020070787B /* OpenCombine in Frameworks */, 6ED82092231676C100B69520 /* CoreLock.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1167,7 +1123,6 @@ 6E6000072121119400787DAA = { isa = PBXGroup; children = ( - 6E8772BB231258CC003B9469 /* JGProgressHUD.xcodeproj */, 6E60004D2121164B00787DAA /* CoreLock.xcodeproj */, 6E6000122121119400787DAA /* SmartLock */, 6E6000272121119500787DAA /* SmartLockUITests */, @@ -1369,15 +1324,6 @@ path = LockSwiftUI; sourceTree = ""; }; - 6E8772BC231258CC003B9469 /* Products */ = { - isa = PBXGroup; - children = ( - 6E8772C5231258CD003B9469 /* JGProgressHUD.framework */, - 6E8772C7231258CD003B9469 /* JGProgressHUD.framework */, - ); - name = Products; - sourceTree = ""; - }; 6E994A85230DFB6C0076DC1B /* LockKit */ = { isa = PBXGroup; children = ( @@ -1692,7 +1638,6 @@ buildRules = ( ); dependencies = ( - 6ECDD09B234D8B3400178B25 /* PBXTargetDependency */, 6E9C960123114D60007C18FE /* PBXTargetDependency */, 6E15AD962318CA4B0004D1AE /* PBXTargetDependency */, 6E994B33230EF6DB0076DC1B /* PBXTargetDependency */, @@ -1742,7 +1687,6 @@ buildRules = ( ); dependencies = ( - 6E83BACC233D906C00D6BA04 /* PBXTargetDependency */, 6E83BACE233D906C00D6BA04 /* PBXTargetDependency */, ); name = LockKitSwiftUI; @@ -1771,7 +1715,6 @@ buildRules = ( ); dependencies = ( - 6E8772D1231265B4003B9469 /* PBXTargetDependency */, 6E8772CD23125A70003B9469 /* PBXTargetDependency */, ); name = LockKit; @@ -1780,7 +1723,6 @@ 6E64B7EC23123846002BC0A2 /* KeychainAccess */, 6E64B7F0231240CC002BC0A2 /* JGProgressHUD */, 6E64B7F3231247EF002BC0A2 /* QRCodeReader */, - 6E5DF8B22319E3A50070787B /* OpenCombine */, 6EE5A10A2329FCA00000235C /* CloudKitCodable */, 6ED44E43235C4406002E8F54 /* SFSafeSymbols */, ); @@ -2029,7 +1971,6 @@ name = "LockKit watchOS"; packageProductDependencies = ( 6ED820992316777A00B69520 /* KeychainAccess */, - 6E5DF8C22319EB020070787B /* OpenCombine */, ); productName = "LockKit watchOS"; productReference = 6ED82086231676AE00B69520 /* LockKit.framework */; @@ -2158,7 +2099,6 @@ 6E64B7EB23123846002BC0A2 /* XCRemoteSwiftPackageReference "KeychainAccess" */, 6E64B7EF231240CC002BC0A2 /* XCRemoteSwiftPackageReference "JGProgressHUD" */, 6E64B7F2231247EF002BC0A2 /* XCRemoteSwiftPackageReference "QRCodeReader.swift" */, - 6E5DF8B12319E3A50070787B /* XCRemoteSwiftPackageReference "OpenCombine" */, 6EE5A1092329FCA00000235C /* XCRemoteSwiftPackageReference "CloudKitCodable" */, 6ED44E3C235C43EE002E8F54 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */, ); @@ -2169,10 +2109,6 @@ ProductGroup = 6E60004E2121164B00787DAA /* Products */; ProjectRef = 6E60004D2121164B00787DAA /* CoreLock.xcodeproj */; }, - { - ProductGroup = 6E8772BC231258CC003B9469 /* Products */; - ProjectRef = 6E8772BB231258CC003B9469 /* JGProgressHUD.xcodeproj */; - }, ); projectRoot = ""; targets = ( @@ -2233,20 +2169,6 @@ remoteRef = 6E60005D2121164B00787DAA /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 6E8772C5231258CD003B9469 /* JGProgressHUD.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = JGProgressHUD.framework; - remoteRef = 6E8772C4231258CD003B9469 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 6E8772C7231258CD003B9469 /* JGProgressHUD.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = JGProgressHUD.framework; - remoteRef = 6E8772C6231258CD003B9469 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ @@ -3088,12 +3010,6 @@ target = 6E60000F2121119400787DAA /* SmartLock */; targetProxy = 6E6000252121119500787DAA /* PBXContainerItemProxy */; }; - 6E83BACC233D906C00D6BA04 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = "JGProgressHUD-iOS"; - platformFilter = ios; - targetProxy = 6E83BACD233D906C00D6BA04 /* PBXContainerItemProxy */; - }; 6E83BACE233D906C00D6BA04 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = "CoreLock-iOS"; @@ -3104,12 +3020,6 @@ name = "CoreLock-iOS"; targetProxy = 6E8772CC23125A70003B9469 /* PBXContainerItemProxy */; }; - 6E8772D1231265B4003B9469 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = "JGProgressHUD-iOS"; - platformFilter = ios; - targetProxy = 6E8772D0231265B4003B9469 /* PBXContainerItemProxy */; - }; 6E994A8F230DFB6D0076DC1B /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 6E994A83230DFB6C0076DC1B /* LockKit */; @@ -3218,12 +3128,6 @@ target = 6E994A83230DFB6C0076DC1B /* LockKit */; targetProxy = 6ECD35CA233B51AE00E49357 /* PBXContainerItemProxy */; }; - 6ECDD09B234D8B3400178B25 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = "JGProgressHUD-iOS"; - platformFilter = ios; - targetProxy = 6ECDD09A234D8B3400178B25 /* PBXContainerItemProxy */; - }; 6ED820962316774E00B69520 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = "CoreLock-watchOS"; @@ -3646,8 +3550,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MACOSX_DEPLOYMENT_TARGET = 10.15; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0.1; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; @@ -3661,8 +3565,8 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TVOS_DEPLOYMENT_TARGET = 11.0; - WATCHOS_DEPLOYMENT_TARGET = 4.0; + TVOS_DEPLOYMENT_TARGET = 15.0; + WATCHOS_DEPLOYMENT_TARGET = 8.0; }; name = Debug; }; @@ -3714,8 +3618,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MACOSX_DEPLOYMENT_TARGET = 10.15; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0.1; MTL_ENABLE_DEBUG_INFO = NO; OTHER_LDFLAGS = ( @@ -3728,9 +3632,9 @@ SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; - TVOS_DEPLOYMENT_TARGET = 11.0; + TVOS_DEPLOYMENT_TARGET = 15.0; VALIDATE_PRODUCT = YES; - WATCHOS_DEPLOYMENT_TARGET = 4.0; + WATCHOS_DEPLOYMENT_TARGET = 8.0; }; name = Release; }; @@ -4691,14 +4595,6 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 6E5DF8B12319E3A50070787B /* XCRemoteSwiftPackageReference "OpenCombine" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/MillerTechnologyPeru/OpenCombine.git"; - requirement = { - branch = master; - kind = branch; - }; - }; 6E64B7E8231236E5002BC0A2 /* XCRemoteSwiftPackageReference "R.swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/mac-cain13/R.swift.Library"; @@ -4798,16 +4694,6 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 6E5DF8B22319E3A50070787B /* OpenCombine */ = { - isa = XCSwiftPackageProductDependency; - package = 6E5DF8B12319E3A50070787B /* XCRemoteSwiftPackageReference "OpenCombine" */; - productName = OpenCombine; - }; - 6E5DF8C22319EB020070787B /* OpenCombine */ = { - isa = XCSwiftPackageProductDependency; - package = 6E5DF8B12319E3A50070787B /* XCRemoteSwiftPackageReference "OpenCombine" */; - productName = OpenCombine; - }; 6E64B7E9231236E5002BC0A2 /* Rswift */ = { isa = XCSwiftPackageProductDependency; package = 6E64B7E8231236E5002BC0A2 /* XCRemoteSwiftPackageReference "R.swift" */; diff --git a/iOS/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/iOS/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a6794efe..719cf0a2 100644 --- a/iOS/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/iOS/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,133 +1,86 @@ { - "object": { - "pins": [ - { - "package": "Bluetooth", - "repositoryURL": "https://github.com/PureSwift/Bluetooth.git", - "state": { - "branch": "master", - "revision": "b03524c7a0c7eca048133a9df9dc4bbe727b38e0", - "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", - "state": { - "branch": "master", - "revision": "598e04c5c6f1162d51fdc45d11c85574d39ee85c", - "version": null - } - }, - { - "package": "CryptoSwift", - "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift", - "state": { - "branch": "master", - "revision": "7c94c0bc9d222f9c88d9f6ed8f71926f372d30a8", - "version": null - } - }, - { - "package": "GATT", - "repositoryURL": "https://github.com/PureSwift/GATT.git", - "state": { - "branch": "master", - "revision": "955a19b2030e08666154f2ee52d7955f56bb6ff5", - "version": null - } - }, - { - "package": "GottaGoFast", - "repositoryURL": "https://github.com/broadwaylamb/GottaGoFast.git", - "state": { - "branch": null, - "revision": "098e92679419e509d42fd97d3e84292acd53e291", - "version": "0.2.0" - } - }, - { - "package": "JGProgressHUD", - "repositoryURL": "https://github.com/MillerTechnologyPeru/JGProgressHUD.git", - "state": { - "branch": "master", - "revision": "a027e0d347c431247c11370b408db567de06e88e", - "version": null - } - }, - { - "package": "KeychainAccess", - "repositoryURL": "https://github.com/MillerTechnologyPeru/KeychainAccess.git", - "state": { - "branch": "master", - "revision": "ee878eaeb3efa09753a9e29e8deb8c331b96f3c8", - "version": null - } - }, - { - "package": "OpenCombine", - "repositoryURL": "https://github.com/MillerTechnologyPeru/OpenCombine.git", - "state": { - "branch": "master", - "revision": "9b9915bde7bcb5c812a577fdd39c1c982668339e", - "version": null - } - }, - { - "package": "QRCodeReader", - "repositoryURL": "https://github.com/MillerTechnologyPeru/QRCodeReader.swift.git", - "state": { - "branch": "master", - "revision": "9db77f45f6f58ad8643f295b7573f0061f03b888", - "version": null - } - }, - { - "package": "R.swift.Library", - "repositoryURL": "https://github.com/mac-cain13/R.swift.Library", - "state": { - "branch": "master", - "revision": "3365947d725398694d6ed49f2e6622f05ca3fc0f", - "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", - "state": { - "branch": "master", - "revision": "8c41d0fd48f6fc00c429aad025c114a941aad764", - "version": null - } - }, - { - "package": "Yams", - "repositoryURL": "https://github.com/jpsim/Yams.git", - "state": { - "branch": null, - "revision": "c947a306d2e80ecb2c0859047b35c73b8e1ca27f", - "version": "2.0.0" - } + "pins" : [ + { + "identity" : "bluetooth", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/Bluetooth.git", + "state" : { + "revision" : "f28f73c1ff5e0877c212300ab356997e7e9570fa", + "version" : "6.1.0" } - ] - }, - "version": 1 + }, + { + "identity" : "cloudkitcodable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/colemancda/CloudKitCodable.git", + "state" : { + "branch" : "master", + "revision" : "598e04c5c6f1162d51fdc45d11c85574d39ee85c" + } + }, + { + "identity" : "gatt", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/GATT.git", + "state" : { + "branch" : "master", + "revision" : "579ffd583f9a32a88e68b69e12eb85856ee165cb" + } + }, + { + "identity" : "jgprogresshud", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MillerTechnologyPeru/JGProgressHUD.git", + "state" : { + "branch" : "master", + "revision" : "a027e0d347c431247c11370b408db567de06e88e" + } + }, + { + "identity" : "keychainaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MillerTechnologyPeru/KeychainAccess.git", + "state" : { + "branch" : "master", + "revision" : "ee878eaeb3efa09753a9e29e8deb8c331b96f3c8" + } + }, + { + "identity" : "qrcodereader.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MillerTechnologyPeru/QRCodeReader.swift.git", + "state" : { + "branch" : "master", + "revision" : "9db77f45f6f58ad8643f295b7573f0061f03b888" + } + }, + { + "identity" : "r.swift.library", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mac-cain13/R.swift.Library", + "state" : { + "branch" : "master", + "revision" : "3a64127e8113cc33179504dfadc91b361d55a0b1" + } + }, + { + "identity" : "sfsafesymbols", + "kind" : "remoteSourceControl", + "location" : "https://github.com/piknotech/SFSafeSymbols.git", + "state" : { + "revision" : "31a99db357f2b839a18ab598c4aa87a741bd6009", + "version" : "1.2.0" + } + }, + { + "identity" : "tlvcoding", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/TLVCoding.git", + "state" : { + "branch" : "master", + "revision" : "49eae2e68320dbe1533eb2880ee82f7b6144517a" + } + } + ], + "version" : 2 } diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Complication).xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Complication).xcscheme index ea9834d5..4dd0fbcc 100644 --- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Complication).xcscheme +++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Complication).xcscheme @@ -55,10 +55,8 @@ debugServiceExtension = "internal" allowLocationSimulation = "YES" launchAutomaticallySubstyle = "32"> - + - + - + - - - - - + diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Notification).xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Notification).xcscheme index edfa09f1..e3463f46 100644 --- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Notification).xcscheme +++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Notification).xcscheme @@ -56,10 +56,8 @@ allowLocationSimulation = "YES" launchAutomaticallySubstyle = "8" notificationPayloadFile = "Watch Extension/PushNotificationPayload.apns"> - + - + - + - - - - - + diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch.xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch.xcscheme index c321ffcf..481525b8 100644 --- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch.xcscheme +++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch.xcscheme @@ -56,10 +56,8 @@ debugServiceExtension = "internal" allowLocationSimulation = "YES" notificationPayloadFile = "Watch Extension/PushNotificationPayload.apns"> - + - + - + - - - - - + diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/WatchIntent.xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/WatchIntent.xcscheme index bb186056..cfed07a6 100644 --- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/WatchIntent.xcscheme +++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/WatchIntent.xcscheme @@ -68,10 +68,8 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> - + - + - + - - - - - + diff --git a/iOS/SmartLock/AppDelegate.swift b/iOS/SmartLock/AppDelegate.swift index e59e2e1a..2715c12d 100644 --- a/iOS/SmartLock/AppDelegate.swift +++ b/iOS/SmartLock/AppDelegate.swift @@ -18,7 +18,7 @@ import GATT import CoreLock import LockKit import JGProgressHUD -import OpenCombine +import Combine @UIApplicationMain final class AppDelegate: UIResponder, UIApplicationDelegate { @@ -41,7 +41,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { private var updateTimer: Timer? #endif - private var locksObserver: OpenCombine.AnyCancellable? + private var locksObserver: Combine.AnyCancellable? // MARK: - UIApplicationDelegate @@ -75,8 +75,10 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { let _ = Store.shared // setup logging - LockManager.shared.log = { log("🔒 LockManager: " + $0) } - LockNetServiceClient.shared.log = { log("🌐 NetService: " + $0) } + #if DEBUG + Store.shared.central.log = { log("📲 Central: " + $0) } + #endif + //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 d8d9682b..1cbca246 100644 --- a/iOS/SmartLock/Controller/KeysViewController.swift +++ b/iOS/SmartLock/Controller/KeysViewController.swift @@ -15,7 +15,7 @@ import GATT import CoreLock import LockKit import JGProgressHUD -import OpenCombine +import Combine import CloudKit import SFSafeSymbols diff --git a/iOS/SmartLock/Controller/NearbyLocksViewController.swift b/iOS/SmartLock/Controller/NearbyLocksViewController.swift index ca7a53e6..01f060ae 100644 --- a/iOS/SmartLock/Controller/NearbyLocksViewController.swift +++ b/iOS/SmartLock/Controller/NearbyLocksViewController.swift @@ -18,7 +18,6 @@ import DarwinGATT import AVFoundation import Intents import JGProgressHUD -import OpenCombine import Combine final class NearbyLocksViewController: UITableViewController { @@ -27,7 +26,7 @@ final class NearbyLocksViewController: UITableViewController { // MARK: - Properties - private var items = [LockPeripheral]() { + private var items = [NativeCentral.Peripheral]() { didSet { tableView.reloadData() } } @@ -47,10 +46,9 @@ final class NearbyLocksViewController: UITableViewController { return feedbackGenerator }() - private var peripheralsObserver: OpenCombine.AnyCancellable? - private var informationObserver: OpenCombine.AnyCancellable? - private var locksObserver: OpenCombine.AnyCancellable? - @available(iOS 13.0, *) + private var peripheralsObserver: Combine.AnyCancellable? + private var informationObserver: Combine.AnyCancellable? + private var locksObserver: Combine.AnyCancellable? private lazy var updateTableViewSubject = Combine.PassthroughSubject() private var updateTableViewObserver: AnyObject? // AnyCancellable @@ -161,9 +159,9 @@ final class NearbyLocksViewController: UITableViewController { BeaconController.shared.scanBeacons() // scan - performActivity(queue: .bluetooth, { - try Store.shared.scan() - for peripheral in Store.shared.peripherals.value.values { + performActivity({ + try await Store.shared.scan() + for peripheral in Store.shared.peripherals.values { do { try Store.shared.readInformation(peripheral) } catch { log("⚠️ Could not read information for peripheral \(peripheral.scanData.peripheral)") @@ -206,19 +204,19 @@ final class NearbyLocksViewController: UITableViewController { let lock = self[indexPath] guard let information = Store.shared.lockInformation.value[lock.scanData.peripheral] else { assertionFailure(); return } - view(lock: information.identifier) + view(lock: information.id) } // MARK: - Private Methods - private subscript (indexPath: IndexPath) -> LockPeripheral { + private subscript (indexPath: IndexPath) -> NativeCentral.Peripheral { return items[indexPath.row] } private func configureView() { // sort by signal - self.items = Store.shared.peripherals.value.values + self.items = Store.shared.peripherals.values .sorted(by: { $0.scanData.rssi < $1.scanData.rssi }) } @@ -246,14 +244,14 @@ final class NearbyLocksViewController: UITableViewController { case .unlock: // known lock - if let lockCache = Store.shared[lock: information.identifier] { + if let lockCache = Store.shared[lock: information.id] { permission = lockCache.key.permission.type title = lockCache.name detail = lockCache.key.permission.localizedText showDetail = true } else { title = R.string.nearbyLocksViewController.lockTitleDefault() // "Lock" - detail = information.identifier.description + detail = information.id.description permission = .anytime showDetail = false } @@ -261,7 +259,7 @@ final class NearbyLocksViewController: UITableViewController { case .setup: title = R.string.nearbyLocksViewController.lockTitleSetup() // "Setup" - detail = information.identifier.description + detail = information.id.description permission = .owner showDetail = false } @@ -293,12 +291,12 @@ final class NearbyLocksViewController: UITableViewController { cell.accessoryType = showDetail ? .detailDisclosureButton : .none } - private func select(_ lock: LockPeripheral) { + private func select(_ lock: NativeCentral.Peripheral) { - guard let information = Store.shared.lockInformation.value[lock.scanData.peripheral] + guard let information = Store.shared.lockInformation[lock.scanData.peripheral] else { return } - let identifier = information.identifier + let identifier = information.id log("Selected lock \(identifier)") diff --git a/iOS/Today/TodayViewController.swift b/iOS/Today/TodayViewController.swift index abc8bed8..38dcd9f8 100644 --- a/iOS/Today/TodayViewController.swift +++ b/iOS/Today/TodayViewController.swift @@ -15,7 +15,7 @@ import GATT import DarwinGATT import CoreLock import LockKit -import OpenCombine +import Combine import Combine final class TodayViewController: UITableViewController { @@ -37,9 +37,9 @@ final class TodayViewController: UITableViewController { return feedbackGenerator }() - private var peripheralsObserver: OpenCombine.AnyCancellable? - private var informationObserver: OpenCombine.AnyCancellable? - private var locksObserver: OpenCombine.AnyCancellable? + private var peripheralsObserver: Combine.AnyCancellable? + private var informationObserver: Combine.AnyCancellable? + private var locksObserver: Combine.AnyCancellable? @available(iOS 13.0, *) private lazy var updateTableViewSubject = Combine.PassthroughSubject() private var updateTableViewObserver: AnyObject? // Combine.AnyCancellable @@ -152,14 +152,14 @@ final class TodayViewController: UITableViewController { .compactMap { Store.shared.lockInformation.value[$0.scanData.peripheral] } .lazy .compactMap { information in - Store.shared[lock: information.identifier] - .flatMap { (identifier: information.identifier, cache: $0) } + Store.shared[lock: information.id] + .flatMap { (identifier: information.id, cache: $0) } } if locks.isEmpty { items = [isScanning ? .loading : .noNearbyLocks] } else { - items = locks.map { .lock($0.identifier, $0.cache) } + items = locks.map { .lock($0.id, $0.cache) } } // Show expanded view for multiple devices diff --git a/iOS/Watch Extension/Controller/InterfaceController.swift b/iOS/Watch Extension/Controller/InterfaceController.swift index 01cc51b0..4cf4c45b 100644 --- a/iOS/Watch Extension/Controller/InterfaceController.swift +++ b/iOS/Watch Extension/Controller/InterfaceController.swift @@ -10,7 +10,7 @@ import Foundation import WatchKit import CoreLock import LockKit -import OpenCombine +import Combine final class InterfaceController: WKInterfaceController { @@ -129,8 +129,8 @@ final class InterfaceController: WKInterfaceController { .flatMap { (device, $0) } } .compactMap { (device, information) in - Store.shared[lock: information.identifier].flatMap { - Item(identifier: information.identifier, cache: $0, peripheral: device) + Store.shared[lock: information.id].flatMap { + Item(identifier: information.id, cache: $0, peripheral: device) } } @@ -157,9 +157,9 @@ final class InterfaceController: WKInterfaceController { private func select(_ item: Item) { - log("Selected lock \(item.identifier)") + log("Selected lock \(item.id)") - unlock(lock: item.identifier, peripheral: item.peripheral) + unlock(lock: item.id, peripheral: item.peripheral) } // MARK: - Segue @@ -195,7 +195,7 @@ private extension InterfaceController { let id: UUID let cache: LockCache - let peripheral: LockPeripheral + let peripheral: NativeCentral.Peripheral } } diff --git a/iOS/Watch Extension/Controller/Unlock.swift b/iOS/Watch Extension/Controller/Unlock.swift index 83477f67..297b2d18 100644 --- a/iOS/Watch Extension/Controller/Unlock.swift +++ b/iOS/Watch Extension/Controller/Unlock.swift @@ -13,11 +13,11 @@ import CoreLock public extension ActivityInterface where Self: WKInterfaceController { - func unlock(lock id: UUID, peripheral: LockPeripheral) { + func unlock(lock id: UUID, peripheral: NativeCentral.Peripheral) { let needsSync: Bool if let lockCache = Store.shared[lock: identifier] { - needsSync = Store.shared[key: lockCache.key.identifier] == nil + needsSync = Store.shared[key: lockCache.key.id] == nil } else { needsSync = true } diff --git a/iOS/Watch Extension/SessionController.swift b/iOS/Watch Extension/SessionController.swift index 4e4b42ad..b44c2416 100644 --- a/iOS/Watch Extension/SessionController.swift +++ b/iOS/Watch Extension/SessionController.swift @@ -269,9 +269,9 @@ public extension Store { let oldApplicationData = self.applicationData var importedKeys = 0 for key in newData.keys { - if self[key: key.identifier] == nil { - let keyData = try session.requestKeyData(for: key.identifier) - self[key: key.identifier] = keyData + if self[key: key.id] == nil { + let keyData = try session.requestKeyData(for: key.id) + self[key: key.id] = keyData importedKeys += 1 } } @@ -295,7 +295,7 @@ public extension Store { // old key no longer exists if newData.keys.contains(oldKey) == false { // remove from keychain - self[key: oldKey.identifier] = nil + self[key: oldKey.id] = nil removedKeys += 1 } } From 469b2f519c80d9f09155f89b91176bedac347f8c Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 20:19:55 -0700 Subject: [PATCH 034/229] [lockd] Fixed Linux compilation --- Sources/{CoreLockGATTServer => lockd}/Error.swift | 4 ++-- Sources/lockd/Extensions/CoreBluetooth.swift | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) rename Sources/{CoreLockGATTServer => lockd}/Error.swift (56%) diff --git a/Sources/CoreLockGATTServer/Error.swift b/Sources/lockd/Error.swift similarity index 56% rename from Sources/CoreLockGATTServer/Error.swift rename to Sources/lockd/Error.swift index 5286c017..40b5551f 100644 --- a/Sources/CoreLockGATTServer/Error.swift +++ b/Sources/lockd/Error.swift @@ -5,7 +5,7 @@ // Created by Alsey Coleman Miller on 8/11/18. // -public enum LockGATTServerError: Error { +public enum LockDaemonError: Error { - case bluetoothUnavailible + case bluetoothUnavailable } diff --git a/Sources/lockd/Extensions/CoreBluetooth.swift b/Sources/lockd/Extensions/CoreBluetooth.swift index ebc5dd1a..62e71189 100644 --- a/Sources/lockd/Extensions/CoreBluetooth.swift +++ b/Sources/lockd/Extensions/CoreBluetooth.swift @@ -11,6 +11,8 @@ import CoreBluetooth import Bluetooth import GATT import DarwinGATT +import CoreLock +import CoreLockGATTServer internal protocol CoreBluetoothManager { @@ -36,9 +38,8 @@ extension CoreBluetoothManager { try await Task.sleep(nanoseconds: 1_000_000_000) powerOnWait += 1 guard powerOnWait < timeout - else { throw BTLEAgentError.bluetoothUnavailable } + else { throw LockDaemonError.bluetoothUnavailable } } } } #endif - From e4e09f3bd084ad6fac819e7f5e47147f1d2fa9fd Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 20:20:31 -0700 Subject: [PATCH 035/229] Ignore VS Code files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 965c5408..5e5d0bbd 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,5 @@ fastlane/test_output *.generated.swift iOS/rswift +# VS Code +.vscode From 0db7696964dab5d8d81f5afab6fb025da172628a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 20:25:35 -0700 Subject: [PATCH 036/229] [LockKit] Fixed watchOS support --- .../Controller/ActivityInterface.swift | 51 +++++++++++++------ iOS/Watch Extension/Controller/Unlock.swift | 6 +-- iOS/Watch Extension/SessionController.swift | 2 +- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/iOS/Watch Extension/Controller/ActivityInterface.swift b/iOS/Watch Extension/Controller/ActivityInterface.swift index 7d7c5df2..f639ded8 100644 --- a/iOS/Watch Extension/Controller/ActivityInterface.swift +++ b/iOS/Watch Extension/Controller/ActivityInterface.swift @@ -9,7 +9,7 @@ import Foundation import WatchKit -public protocol ActivityInterface: class { +public protocol ActivityInterface: AnyObject { var activityImageView: WKInterfaceImage? { get } @@ -48,38 +48,57 @@ public extension ActivityInterface where Self: WKInterfaceController { completion: ((Self, T) -> ())? = nil) { assert(Thread.isMainThread) - let queue = queue ?? .app - if showActivity { self.showActivity() } - queue.async { - do { - let value = try asyncOperation() - mainQueue { [weak self] in - guard let controller = self else { return } - if showActivity { controller.hideActivity() } - // success completion?(controller, value) } + } catch { + mainQueue { [weak self] in + guard let controller = self + else { return } + if showActivity { controller.hideActivity() } + // show error + log("⚠️ Error: \(error.localizedDescription)") + #if DEBUG + print(error) + #endif + controller.showError(error.localizedDescription) + } } + } + } + + func performActivity (showActivity: Bool = true, + _ asyncOperation: @escaping () async throws -> T, + completion: ((Self, T) -> ())? = nil) { + + assert(Thread.isMainThread) - catch { - - mainQueue { [weak self] in - + if showActivity { self.showActivity() } + + Task { + do { + let value = try await asyncOperation() + await MainActor.run { [weak self] in + guard let controller = self + else { return } + if showActivity { controller.hideActivity() } + // success + completion?(controller, value) + } + } catch { + await MainActor.run { [weak self] in guard let controller = self else { return } - if showActivity { controller.hideActivity() } - // show error log("⚠️ Error: \(error.localizedDescription)") #if DEBUG diff --git a/iOS/Watch Extension/Controller/Unlock.swift b/iOS/Watch Extension/Controller/Unlock.swift index 297b2d18..2550eb0f 100644 --- a/iOS/Watch Extension/Controller/Unlock.swift +++ b/iOS/Watch Extension/Controller/Unlock.swift @@ -16,7 +16,7 @@ public extension ActivityInterface where Self: WKInterfaceController { func unlock(lock id: UUID, peripheral: NativeCentral.Peripheral) { let needsSync: Bool - if let lockCache = Store.shared[lock: identifier] { + if let lockCache = Store.shared[lock: id] { needsSync = Store.shared[key: lockCache.key.id] == nil } else { needsSync = true @@ -24,8 +24,8 @@ public extension ActivityInterface where Self: WKInterfaceController { if needsSync { Store.shared.syncApp() } - performActivity(queue: .bluetooth, { - try Store.shared.unlock(peripheral) + performActivity({ + try await Store.shared.unlock(peripheral) }, completion: { (controller, hasKey) in let haptic: WKHapticType = hasKey ? .success : .failure WKInterfaceDevice.current().play(haptic) diff --git a/iOS/Watch Extension/SessionController.swift b/iOS/Watch Extension/SessionController.swift index b44c2416..cf9079c1 100644 --- a/iOS/Watch Extension/SessionController.swift +++ b/iOS/Watch Extension/SessionController.swift @@ -70,7 +70,7 @@ public final class SessionController: NSObject { public func requestKeyData(for id: UUID) throws -> KeyData { - let response = try request(.key(identifier)) + let response = try request(.key(id)) switch response { case let .error(error): throw Error.errorResponse(error) From 6d5a9fa24fe7ca24de3f35cbf533d926ce2d392b Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 20:36:21 -0700 Subject: [PATCH 037/229] Updated dependencies --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Xcode/CoreLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcode/CoreLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5be943f4..fbe87826 100644 --- a/Xcode/CoreLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Xcode/CoreLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/PureSwift/Bluetooth.git", "state" : { - "revision" : "f28f73c1ff5e0877c212300ab356997e7e9570fa", - "version" : "6.1.0" + "revision" : "f3b0af86ebf973aab7d51f96a1cdaae7ee876635", + "version" : "6.0.5" } }, { diff --git a/iOS/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/iOS/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 719cf0a2..d107d64a 100644 --- a/iOS/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/iOS/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/PureSwift/Bluetooth.git", "state" : { - "revision" : "f28f73c1ff5e0877c212300ab356997e7e9570fa", - "version" : "6.1.0" + "revision" : "f3b0af86ebf973aab7d51f96a1cdaae7ee876635", + "version" : "6.0.5" } }, { From 1e54b1a2f0f614515198adaffae640d5be04e0d6 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 21:28:53 -0700 Subject: [PATCH 038/229] Updated iOS App for Swift 5.7 --- iOS/Intent/IntentHandler.swift | 26 ++--- iOS/IntentUI/IntentViewController.swift | 2 +- iOS/LockKit/Model/Queue.swift | 1 - iOS/LockKit/Model/Store.swift | 6 +- iOS/Message/MessagesViewController.swift | 6 +- iOS/QuickLook/PreviewViewController.swift | 6 +- iOS/SmartLock.xcodeproj/project.pbxproj | 103 +++++++++++++----- iOS/SmartLock/AppDelegate.swift | 66 +++++------ .../Controller/KeysViewController.swift | 10 +- .../NearbyLocksViewController.swift | 43 ++++---- iOS/Today/TodayViewController.swift | 27 +++-- .../Controller/InterfaceController.swift | 57 +++++----- iOS/Watch Extension/ExtensionDelegate.swift | 6 +- 13 files changed, 208 insertions(+), 151 deletions(-) diff --git a/iOS/Intent/IntentHandler.swift b/iOS/Intent/IntentHandler.swift index 69cc1d76..be0cb2b6 100644 --- a/iOS/Intent/IntentHandler.swift +++ b/iOS/Intent/IntentHandler.swift @@ -31,7 +31,7 @@ final class IntentHandler: INExtension { assert(Thread.isMainThread == false) DispatchQueue.main.sync { let _ = IntentHandler.didLaunch - LockManager.shared.log = { log("🔒 LockManager: " + $0) } + Store.shared.central.log = { log("📲 Central: " + $0) } #if os(iOS) BeaconController.shared.log = { log("📶 \(BeaconController.self): " + $0) } #endif @@ -39,7 +39,7 @@ final class IntentHandler: INExtension { Store.shared.loadCache() } - log("🎙 Handle intent \(intent.intentDescription ?? intent.id ?? intent.description)") + log("🎙 Handle intent \(intent.intentDescription ?? intent.identifier ?? intent.description)") if #available(watchOSApplicationExtension 5.0, *) { return UnlockIntentHandler() @@ -62,7 +62,7 @@ final class UnlockIntentHandler: NSObject, UnlockIntentHandling { Store.shared.loadCache() // locks - let intentLocks = Store.shared.locks.value.map { IntentLock(identifier: $0, name: $1.name) } + let intentLocks = Store.shared.locks.map { IntentLock(id: $0, name: $1.name) } completion(intentLocks, nil) } } @@ -70,7 +70,7 @@ final class UnlockIntentHandler: NSObject, UnlockIntentHandling { @available(iOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) func resolveLock(for intent: UnlockIntent, with completion: @escaping (UnlockLockResolutionResult) -> Void) { - DispatchQueue.bluetooth.async { + Task { // load updated lock information Store.shared.loadCache() @@ -82,7 +82,7 @@ final class UnlockIntentHandler: NSObject, UnlockIntentHandling { } // validate UUID string - guard let identifier = intentLock.id.flatMap({ UUID(uuidString: $0) }) else { + guard let identifier = intentLock.identifier.flatMap({ UUID(uuidString: $0) }) else { completion(.unsupported()) return } @@ -96,7 +96,7 @@ final class UnlockIntentHandler: NSObject, UnlockIntentHandling { // check if lock is in range var device: NativeCentral.Peripheral? - do { device = try Store.shared.device(for: identifier, scanDuration: 2.0) } + do { device = try await Store.shared.device(for: identifier, scanDuration: 2.0) } catch { completion(.confirmationRequired(with: intentLock)) return @@ -116,7 +116,7 @@ final class UnlockIntentHandler: NSObject, UnlockIntentHandling { func confirm(intent: UnlockIntent, completion: @escaping (UnlockIntentResponse) -> Void) { assert(Thread.isMainThread == false, "Should not be main thread") - + /* mainQueue { guard let intentLock = intent.lock else { @@ -132,14 +132,14 @@ final class UnlockIntentHandler: NSObject, UnlockIntentHandling { } completion(.init(code: .ready, userActivity: NSUserActivity(.action(.unlock(lockIdentifier))))) - } + }*/ } @available(iOSApplicationExtension 12.0, *) func handle(intent: UnlockIntent, completion: @escaping (UnlockIntentResponse) -> Void) { assert(Thread.isMainThread == false, "Should not be main thread") - + /* mainQueue { guard let intentLock = intent.lock else { @@ -162,14 +162,14 @@ final class UnlockIntentHandler: NSObject, UnlockIntentHandling { } } - DispatchQueue.bluetooth.async { + Task { do { - guard let peripheral = try Store.shared.device(for: lockIdentifier, scanDuration: 1.0) else { + guard let peripheral = try await Store.shared.device(for: lockIdentifier, scanDuration: 1.0) else { completion(.failure(failureReason: "Lock not in range. ")) return } - guard try Store.shared.unlock(peripheral) else { + guard try await Store.shared.unlock(peripheral) else { completion(.failure(failureReason: "Invalid lock.")) return } @@ -181,7 +181,7 @@ final class UnlockIntentHandler: NSObject, UnlockIntentHandling { return } } - } + }*/ } } diff --git a/iOS/IntentUI/IntentViewController.swift b/iOS/IntentUI/IntentViewController.swift index 1da787b5..04ebb64c 100644 --- a/iOS/IntentUI/IntentViewController.swift +++ b/iOS/IntentUI/IntentViewController.swift @@ -47,7 +47,7 @@ final class IntentViewController: UIViewController, INUIHostedViewControlling { Store.shared.loadCache() guard let intent = interaction.intent as? UnlockIntent, - let lockIdentifierString = intent.lock?.id, + let lockIdentifierString = intent.lock?.identifier, let lockIdentifier = UUID(uuidString: lockIdentifierString), let lockCache = FileManager.Lock.shared.applicationData?.locks[lockIdentifier] else { completion(false, [], .zero) diff --git a/iOS/LockKit/Model/Queue.swift b/iOS/LockKit/Model/Queue.swift index 48f8df9c..8d7ea53c 100644 --- a/iOS/LockKit/Model/Queue.swift +++ b/iOS/LockKit/Model/Queue.swift @@ -9,7 +9,6 @@ import Foundation /// Perform task on main queue -@inline(__always) public func mainQueue(_ block: @escaping () -> ()) { DispatchQueue.main.async(execute: block) } diff --git a/iOS/LockKit/Model/Store.swift b/iOS/LockKit/Model/Store.swift index dde217a9..e9438ee7 100644 --- a/iOS/LockKit/Model/Store.swift +++ b/iOS/LockKit/Model/Store.swift @@ -451,7 +451,11 @@ public extension Store { let duration = duration ?? preferences.scanDuration let filterDuplicates = preferences.filterDuplicates self.peripherals.removeAll() - let stream = central.scan(with: [LockService.uuid]) + let stream = central.scan(with: [LockService.uuid], filterDuplicates: filterDuplicates) + Task { + try? await Task.sleep(nanoseconds: UInt64(Int64(duration)) * 1_000_000_000) + stream.stop() + } for try await scanData in stream { guard let serviceUUIDs = scanData.advertisementData.serviceUUIDs, serviceUUIDs.contains(LockService.uuid) diff --git a/iOS/Message/MessagesViewController.swift b/iOS/Message/MessagesViewController.swift index bcb32360..a43e2ea2 100644 --- a/iOS/Message/MessagesViewController.swift +++ b/iOS/Message/MessagesViewController.swift @@ -41,7 +41,7 @@ final class MessagesViewController: MSMessagesAppViewController { log("✉️ Loaded \(MessagesViewController.self)") // setup loading - LockManager.shared.log = { log("🔒 LockManager: " + $0) } + Store.shared.central.log = { log("📲 Central: " + $0) } BeaconController.shared.log = { log("📶 \(BeaconController.self): " + $0) } SpotlightController.shared.log = { log("🔦 \(SpotlightController.self): " + $0) } @@ -168,11 +168,11 @@ final class MessagesViewController: MSMessagesAppViewController { Store.shared.loadCache() // set data - self.items = Store.shared.locks.value + self.items = Store.shared.locks .lazy .filter { $0.value.key.permission.isAdministrator } .lazy - .map { Item(identifier: $0.key, cache: $0.value) } + .map { Item(id: $0.key, cache: $0.value) } .sorted(by: { $0.cache.key.created < $1.cache.key.created }) // update table view diff --git a/iOS/QuickLook/PreviewViewController.swift b/iOS/QuickLook/PreviewViewController.swift index 7f4e6880..1747fc01 100644 --- a/iOS/QuickLook/PreviewViewController.swift +++ b/iOS/QuickLook/PreviewViewController.swift @@ -30,11 +30,9 @@ final class PreviewViewController: UIViewController, QLPreviewingController { log("👁‍🗨 Loaded \(PreviewViewController.self)") // setup logging - LockManager.shared.log = { log("🔒 LockManager: " + $0) } + Store.shared.central.log = { log("📲 Central: " + $0) } BeaconController.shared.log = { log("📶 \(BeaconController.self): " + $0) } SpotlightController.shared.log = { log("🔦 \(SpotlightController.self): " + $0) } - - } // MARK: - QLPreviewingController @@ -107,7 +105,7 @@ private extension PreviewViewController { func loadLock(_ id: UUID) { // load view controller - let viewController = LockViewController.fromStoryboard(with: identifier) + let viewController = LockViewController.fromStoryboard(with: id) loadChildViewController(viewController) } diff --git a/iOS/SmartLock.xcodeproj/project.pbxproj b/iOS/SmartLock.xcodeproj/project.pbxproj index 93f65705..84d3f426 100644 --- a/iOS/SmartLock.xcodeproj/project.pbxproj +++ b/iOS/SmartLock.xcodeproj/project.pbxproj @@ -78,7 +78,6 @@ 6E6000292121119500787DAA /* SmartLockUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6000282121119500787DAA /* SmartLockUITests.swift */; }; 6E64B7EA231236E5002BC0A2 /* Rswift in Frameworks */ = {isa = PBXBuildFile; productRef = 6E64B7E9231236E5002BC0A2 /* Rswift */; }; 6E64B7ED23123846002BC0A2 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 6E64B7EC23123846002BC0A2 /* KeychainAccess */; }; - 6E64B7F1231240CC002BC0A2 /* JGProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = 6E64B7F0231240CC002BC0A2 /* JGProgressHUD */; }; 6E64B7F4231247EF002BC0A2 /* QRCodeReader in Frameworks */ = {isa = PBXBuildFile; platformFilter = ios; productRef = 6E64B7F3231247EF002BC0A2 /* QRCodeReader */; }; 6E6B78B62325F14600635EEF /* SliderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E135CBD2319842A00FFA7F7 /* SliderCell.swift */; }; 6E6B78B72325F17300635EEF /* BluetoothSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E135CB72319832900FFA7F7 /* BluetoothSettingsView.swift */; }; @@ -92,6 +91,9 @@ 6E7374602325EA1600EC85B2 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E73745E2325EA1600EC85B2 /* Event.swift */; }; 6E7503C32318E78400103EBB /* WatchApplicationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7503C22318E78400103EBB /* WatchApplicationContext.swift */; }; 6E7503C42318E78400103EBB /* WatchApplicationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7503C22318E78400103EBB /* WatchApplicationContext.swift */; }; + 6E7E787528D6CACD00B81B65 /* JGProgressHUD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E7E787228D6CAAB00B81B65 /* JGProgressHUD.framework */; }; + 6E7E787628D6CACD00B81B65 /* JGProgressHUD.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6E7E787228D6CAAB00B81B65 /* JGProgressHUD.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 6E7E787828D6D37E00B81B65 /* WatchIntent.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6ED8206C2316711900B69520 /* WatchIntent.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 6E806B8C230F4F2800C6FF78 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E806B8B230F4F2800C6FF78 /* Preferences.swift */; }; 6E806B94230F4F4700C6FF78 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E806B93230F4F4700C6FF78 /* Keychain.swift */; }; 6E806B96230F4F7300C6FF78 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E806B95230F4F7300C6FF78 /* AppGroup.swift */; }; @@ -107,7 +109,6 @@ 6E806BB3230F6EC400C6FF78 /* R.LockKit.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E806BB2230F6EC300C6FF78 /* R.LockKit.generated.swift */; }; 6E806BD92310F36E00C6FF78 /* R.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E806BD82310F36E00C6FF78 /* R.swift */; }; 6E806BDB2310FB2C00C6FF78 /* PermissionIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E806BDA2310FB2C00C6FF78 /* PermissionIconView.swift */; }; - 6E81A3262332E26800F73AFC /* WatchIntent.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6ED8206C2316711900B69520 /* WatchIntent.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 6E83BA6D233CBAF400D6BA04 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E83BA6C233CBAF400D6BA04 /* Error.swift */; }; 6E83BA6E233CBAF400D6BA04 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E83BA6C233CBAF400D6BA04 /* Error.swift */; }; 6E83BA77233CC00B00D6BA04 /* NearbyLocksViewController.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6E83BA79233CC00B00D6BA04 /* NearbyLocksViewController.strings */; }; @@ -231,7 +232,6 @@ 6E9794CC23315E2E00B5C5C9 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EB59D5E2312E9E400D5EFA7 /* CloudKit.framework */; }; 6E9794D123315E7400B5C5C9 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EB59D5E2312E9E400D5EFA7 /* CloudKit.framework */; }; 6E9794D223315E8100B5C5C9 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EB59D5E2312E9E400D5EFA7 /* CloudKit.framework */; }; - 6E9794D423315EA000B5C5C9 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E9794D323315EA000B5C5C9 /* CloudKit.framework */; }; 6E9794D523315EB000B5C5C9 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E9794D323315EA000B5C5C9 /* CloudKit.framework */; }; 6E9794D623319DC900B5C5C9 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E60F8AC212127890054995D /* Log.swift */; }; 6E9794D72331D05B00B5C5C9 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EB59D5E2312E9E400D5EFA7 /* CloudKit.framework */; }; @@ -419,6 +419,27 @@ remoteGlobalIDString = DD75027A1C68FCFC006590AF; remoteInfo = CoreLockTests; }; + 6E7E787128D6CAAB00B81B65 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6E7E786C28D6CAAB00B81B65 /* JGProgressHUD.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 6BF7DC5A1AEFCFD9000A0AE0; + remoteInfo = "JGProgressHUD-iOS"; + }; + 6E7E787328D6CAAB00B81B65 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6E7E786C28D6CAAB00B81B65 /* JGProgressHUD.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 6BABF9A31F7A6133005E783F; + remoteInfo = "JGProgressHUD-tvOS"; + }; + 6E7E787928D6D37E00B81B65 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6E6000082121119400787DAA /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6ED8206B2316711900B69520; + remoteInfo = WatchIntent; + }; 6E83BACF233D906C00D6BA04 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 6E60004D2121164B00787DAA /* CoreLock.xcodeproj */; @@ -602,15 +623,26 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; - 6E81A31E2332E24000F73AFC /* Embed App Extensions */ = { + 6E7E787728D6CACD00B81B65 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 6E7E787628D6CACD00B81B65 /* JGProgressHUD.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + 6E7E787B28D6D37E00B81B65 /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( - 6E81A3262332E26800F73AFC /* WatchIntent.appex in Embed App Extensions */, + 6E7E787828D6D37E00B81B65 /* WatchIntent.appex in Embed Foundation Extensions */, ); - name = "Embed App Extensions"; + name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; }; 6E994AF6230E56DD0076DC1B /* Embed App Extensions */ = { @@ -765,6 +797,7 @@ 6E7374572325E11300EC85B2 /* NewKeyManagedObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyManagedObject.swift; sourceTree = ""; }; 6E73745E2325EA1600EC85B2 /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; 6E7503C22318E78400103EBB /* WatchApplicationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchApplicationContext.swift; sourceTree = ""; }; + 6E7E786C28D6CAAB00B81B65 /* JGProgressHUD.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = JGProgressHUD.xcodeproj; path = JGProgressHUD/JGProgressHUD.xcodeproj; sourceTree = ""; }; 6E806B8B230F4F2800C6FF78 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; 6E806B93230F4F4700C6FF78 /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; 6E806B95230F4F7300C6FF78 /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = ""; }; @@ -961,7 +994,7 @@ files = ( 6E64B7EA231236E5002BC0A2 /* Rswift in Frameworks */, 6E64B7F4231247EF002BC0A2 /* QRCodeReader in Frameworks */, - 6E64B7F1231240CC002BC0A2 /* JGProgressHUD in Frameworks */, + 6E7E787528D6CACD00B81B65 /* JGProgressHUD.framework in Frameworks */, 6ED44E44235C4406002E8F54 /* SFSafeSymbols in Frameworks */, 6EE5A10B2329FCA00000235C /* CloudKitCodable in Frameworks */, 6E64B7ED23123846002BC0A2 /* KeychainAccess in Frameworks */, @@ -1015,7 +1048,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6E9794D423315EA000B5C5C9 /* CloudKit.framework in Frameworks */, 6EA570F223189CE9008027D4 /* CoreLock.framework in Frameworks */, 6EA570EE23189CE9008027D4 /* LockKit.framework in Frameworks */, ); @@ -1123,6 +1155,7 @@ 6E6000072121119400787DAA = { isa = PBXGroup; children = ( + 6E7E786C28D6CAAB00B81B65 /* JGProgressHUD.xcodeproj */, 6E60004D2121164B00787DAA /* CoreLock.xcodeproj */, 6E6000122121119400787DAA /* SmartLock */, 6E6000272121119500787DAA /* SmartLockUITests */, @@ -1292,6 +1325,15 @@ path = Extensions; sourceTree = ""; }; + 6E7E786D28D6CAAB00B81B65 /* Products */ = { + isa = PBXGroup; + children = ( + 6E7E787228D6CAAB00B81B65 /* JGProgressHUD.framework */, + 6E7E787428D6CAAB00B81B65 /* JGProgressHUD.framework */, + ); + name = Products; + sourceTree = ""; + }; 6E806BA7230F654600C6FF78 /* View */ = { isa = PBXGroup; children = ( @@ -1711,6 +1753,7 @@ 6E994A80230DFB6C0076DC1B /* Sources */, 6E994A81230DFB6C0076DC1B /* Frameworks */, 6E994A82230DFB6C0076DC1B /* Resources */, + 6E7E787728D6CACD00B81B65 /* Embed Frameworks */, ); buildRules = ( ); @@ -1721,7 +1764,6 @@ packageProductDependencies = ( 6E64B7E9231236E5002BC0A2 /* Rswift */, 6E64B7EC23123846002BC0A2 /* KeychainAccess */, - 6E64B7F0231240CC002BC0A2 /* JGProgressHUD */, 6E64B7F3231247EF002BC0A2 /* QRCodeReader */, 6EE5A10A2329FCA00000235C /* CloudKitCodable */, 6ED44E43235C4406002E8F54 /* SFSafeSymbols */, @@ -1859,13 +1901,14 @@ 6EA570CD23189874008027D4 /* Frameworks */, 6EA570CE23189874008027D4 /* Resources */, 6EA570F423189CE9008027D4 /* Embed Frameworks */, - 6E81A31E2332E24000F73AFC /* Embed App Extensions */, 6E3D1DF42334BDCB000C6E06 /* Build Version */, + 6E7E787B28D6D37E00B81B65 /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( 6EA570F123189CE9008027D4 /* PBXTargetDependency */, + 6E7E787A28D6D37E00B81B65 /* PBXTargetDependency */, ); name = "Watch Extension"; packageProductDependencies = ( @@ -2109,6 +2152,10 @@ ProductGroup = 6E60004E2121164B00787DAA /* Products */; ProjectRef = 6E60004D2121164B00787DAA /* CoreLock.xcodeproj */; }, + { + ProductGroup = 6E7E786D28D6CAAB00B81B65 /* Products */; + ProjectRef = 6E7E786C28D6CAAB00B81B65 /* JGProgressHUD.xcodeproj */; + }, ); projectRoot = ""; targets = ( @@ -2169,6 +2216,20 @@ remoteRef = 6E60005D2121164B00787DAA /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 6E7E787228D6CAAB00B81B65 /* JGProgressHUD.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = JGProgressHUD.framework; + remoteRef = 6E7E787128D6CAAB00B81B65 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 6E7E787428D6CAAB00B81B65 /* JGProgressHUD.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = JGProgressHUD.framework; + remoteRef = 6E7E787328D6CAAB00B81B65 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ @@ -3010,6 +3071,11 @@ target = 6E60000F2121119400787DAA /* SmartLock */; targetProxy = 6E6000252121119500787DAA /* PBXContainerItemProxy */; }; + 6E7E787A28D6D37E00B81B65 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6ED8206B2316711900B69520 /* WatchIntent */; + targetProxy = 6E7E787928D6D37E00B81B65 /* PBXContainerItemProxy */; + }; 6E83BACE233D906C00D6BA04 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = "CoreLock-iOS"; @@ -3460,7 +3526,6 @@ "@executable_path/../Frameworks", "@executable_path/../../../../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.12; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = maccatalyst.com.colemancda.Lock.Today; @@ -3486,7 +3551,6 @@ "@executable_path/../Frameworks", "@executable_path/../../../../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.12; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = maccatalyst.com.colemancda.Lock.Today; PRODUCT_NAME = Today; @@ -3744,7 +3808,6 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = LockKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "${BUILT_PRODUCTS_DIR}", @@ -3777,7 +3840,6 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = LockKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3903,7 +3965,6 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 4W79SG34MW; INFOPLIST_FILE = IntentUI/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3927,7 +3988,6 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 4W79SG34MW; INFOPLIST_FILE = IntentUI/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3950,7 +4010,6 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 4W79SG34MW; INFOPLIST_FILE = Intent/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3974,7 +4033,6 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 4W79SG34MW; INFOPLIST_FILE = Intent/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4228,7 +4286,6 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 4W79SG34MW; INFOPLIST_FILE = QuickLook/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4254,7 +4311,6 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 4W79SG34MW; INFOPLIST_FILE = QuickLook/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4279,7 +4335,6 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 4W79SG34MW; INFOPLIST_FILE = Thumbnail/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4304,7 +4359,6 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 4W79SG34MW; INFOPLIST_FILE = Thumbnail/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4704,11 +4758,6 @@ package = 6E64B7EB23123846002BC0A2 /* XCRemoteSwiftPackageReference "KeychainAccess" */; productName = KeychainAccess; }; - 6E64B7F0231240CC002BC0A2 /* JGProgressHUD */ = { - isa = XCSwiftPackageProductDependency; - package = 6E64B7EF231240CC002BC0A2 /* XCRemoteSwiftPackageReference "JGProgressHUD" */; - productName = JGProgressHUD; - }; 6E64B7F3231247EF002BC0A2 /* QRCodeReader */ = { isa = XCSwiftPackageProductDependency; package = 6E64B7F2231247EF002BC0A2 /* XCRemoteSwiftPackageReference "QRCodeReader.swift" */; diff --git a/iOS/SmartLock/AppDelegate.swift b/iOS/SmartLock/AppDelegate.swift index 2715c12d..f9e58d59 100644 --- a/iOS/SmartLock/AppDelegate.swift +++ b/iOS/SmartLock/AppDelegate.swift @@ -111,7 +111,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { WatchController.shared.context = .init( applicationData: Store.shared.applicationData ) - locksObserver = Store.shared.locks.sink { _ in + locksObserver = Store.shared.$locks.sink { _ in WatchController.shared.context = .init( applicationData: Store.shared.applicationData ) @@ -168,19 +168,19 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { guard ProcessInfo.processInfo.isLowPowerModeEnabled == false else { return } // scan in background - if Store.shared.lockManager.central.state == .poweredOn { - let bluetoothTask = application.beginBackgroundTask(withName: "BluetoothScan", expirationHandler: { - log("\(bundle.symbol) Background task expired") - }) - DispatchQueue.bluetooth.async { [unowned self] in + Task { + if await Store.shared.central.state == .poweredOn { + let bluetoothTask = application.beginBackgroundTask(withName: "BluetoothScan", expirationHandler: { + log("\(bundle.symbol) Background task expired") + }) // scan for nearby devices - do { try Store.shared.scan(duration: 3.0) } + do { try await Store.shared.scan(duration: 3.0) } catch { log("⚠️ Unable to scan: \(error.localizedDescription)") } // read information characteristic - for device in Store.shared.peripherals.value.values { - guard Store.shared.lockInformation.value[device.scanData.peripheral] == nil + for device in Store.shared.peripherals.keys { + guard Store.shared.lockInformation[device] == nil else { continue } - do { try Store.shared.readInformation(device) } + do { try await Store.shared.readInformation(device) } catch { log("⚠️ Unable to read information: \(error.localizedDescription)") } } DispatchQueue.main.async { [weak self] in @@ -191,6 +191,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { } } + /* // attempt to sync with iCloud in background let cloudTask = application.beginBackgroundTask(withName: "iCloudSync", expirationHandler: { log("\(bundle.symbol) Background task expired") @@ -203,7 +204,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { log("\(bundle.symbol) iCloud background task ended") application.endBackgroundTask(cloudTask) } - } + }*/ } func applicationWillEnterForeground(_ application: UIApplication) { @@ -217,19 +218,19 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { // save energy guard ProcessInfo.processInfo.isLowPowerModeEnabled == false else { return } - // attempt to scan for all known locks if they are not in central cache - if Store.shared.lockManager.central.state == .poweredOn { - DispatchQueue.bluetooth.async { - let locks = Store.shared.locks.value.keys + Task { + // attempt to scan for all known locks if they are not in central cache + if await Store.shared.central.state == .poweredOn { + let locks = Store.shared.locks.keys guard locks.contains(where: { Store.shared.device(for: $0) == nil }) else { return } // scan for nearby devices - do { try Store.shared.scan(duration: 3.0) } + do { try await Store.shared.scan(duration: 3.0) } catch { log("⚠️ Unable to scan: \(error.localizedDescription)") } // read information characteristic - for device in Store.shared.peripherals.value.values { - guard Store.shared.lockInformation.value[device.scanData.peripheral] == nil + for device in Store.shared.peripherals.keys { + guard Store.shared.lockInformation[device] == nil else { continue } - do { try Store.shared.readInformation(device) } + do { try await Store.shared.readInformation(device) } catch { log("⚠️ Unable to read information: \(error.localizedDescription)") } } } @@ -291,13 +292,13 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { // 30 sec max background fetch var result: UIBackgroundFetchResult = .noData let applicationData = Store.shared.applicationData - let information = Array(Store.shared.lockInformation.value.values) - DispatchQueue.bluetooth.async { [unowned self] in + let information = Array(Store.shared.lockInformation.values) + Task { [unowned self] in do { // scan for locks - try Store.shared.scan(duration: 5.0) + try await Store.shared.scan(duration: 5.0) // make sure each stored lock is visible - let locks = Store.shared.locks.value + let locks = Store.shared.locks .lazy .sorted(by: { $0.value.key.created < $1.value.key.created }) .lazy @@ -307,12 +308,12 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { .prefix(10) // scan for locks not found for lock in locks { - let _ = try Store.shared.device(for: lock, scanDuration: 1.0) + let _ = try await Store.shared.device(for: lock, scanDuration: 1.0) } } catch { log("⚠️ Unable to scan: \(error.localizedDescription)") result = .failed - } + }/* // attempt to sync with iCloud DispatchQueue.cloud.async { do { try Store.shared.syncCloud() } @@ -322,7 +323,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { } if result != .failed { if applicationData == Store.shared.applicationData, - information == Array(Store.shared.lockInformation.value.values) { + information == Array(Store.shared.lockInformation.values) { result = .noData } else { result = .newData @@ -331,7 +332,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { mainQueue { self.logBackgroundTimeRemaining() } log("\(bundle.symbol) Background fetch ended") completionHandler(result) - } + }*/ } } @@ -519,15 +520,16 @@ private extension AppDelegate { let bundle = self.bundle log("\(bundle.symbol) Will update data") - DispatchQueue.bluetooth.async { + Task { do { // scan for locks - try Store.shared.scan(duration: 3.0) + try await Store.shared.scan(duration: 3.0) // make sure each stored lock is visible - for lock in Store.shared.locks.value.keys { - let _ = try Store.shared.device(for: lock, scanDuration: 1.0) + for lock in Store.shared.locks.keys { + let _ = try await Store.shared.device(for: lock, scanDuration: 1.0) } } catch { log("⚠️ Unable to scan: \(error.localizedDescription)") } + /* // attempt to sync with iCloud DispatchQueue.cloud.async { do { try Store.shared.syncCloud() } @@ -535,7 +537,7 @@ private extension AppDelegate { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { log("\(bundle.symbol) Updated data") } - } + }*/ } } } diff --git a/iOS/SmartLock/Controller/KeysViewController.swift b/iOS/SmartLock/Controller/KeysViewController.swift index 1cbca246..def9a8cb 100644 --- a/iOS/SmartLock/Controller/KeysViewController.swift +++ b/iOS/SmartLock/Controller/KeysViewController.swift @@ -58,7 +58,7 @@ final class KeysViewController: UITableViewController { tableView.estimatedRowHeight = 60 // load data - locksObserver = Store.shared.locks.sink { locks in + locksObserver = Store.shared.$locks.sink { locks in mainQueue { [weak self] in self?.configureView() } } } @@ -230,13 +230,13 @@ final class KeysViewController: UITableViewController { @discardableResult internal func select(lock id: UUID, animated: Bool = true) -> LockViewController? { - guard Store.shared[lock: identifier] != nil else { - showErrorAlert(LockError.noKey(lock: identifier).localizedDescription) + guard Store.shared[lock: id] != nil else { + showErrorAlert(LockError.noKey(lock: id).localizedDescription) return nil } - let lockViewController = LockViewController.fromStoryboard(with: identifier) - lockViewController.lockIdentifier = identifier + let lockViewController = LockViewController.fromStoryboard(with: id) + lockViewController.lockIdentifier = id if animated { self.show(lockViewController, sender: self) } else if let navigationController = self.navigationController { diff --git a/iOS/SmartLock/Controller/NearbyLocksViewController.swift b/iOS/SmartLock/Controller/NearbyLocksViewController.swift index 01f060ae..4bd352c7 100644 --- a/iOS/SmartLock/Controller/NearbyLocksViewController.swift +++ b/iOS/SmartLock/Controller/NearbyLocksViewController.swift @@ -63,13 +63,13 @@ final class NearbyLocksViewController: UITableViewController { tableView.estimatedRowHeight = 60 // observe model changes - peripheralsObserver = Store.shared.peripherals.sink { [weak self] _ in + peripheralsObserver = Store.shared.$peripherals.sink { [weak self] _ in self?.locksChanged() } - informationObserver = Store.shared.lockInformation.sink { [weak self] _ in + informationObserver = Store.shared.$lockInformation.sink { [weak self] _ in self?.locksChanged() } - locksObserver = Store.shared.locks.sink { [weak self] _ in + locksObserver = Store.shared.$locks.sink { [weak self] _ in self?.locksChanged() } @@ -84,7 +84,7 @@ final class NearbyLocksViewController: UITableViewController { #if !targetEnvironment(macCatalyst) // scan if none is setup - if Store.shared.locks.value.isEmpty { + if Store.shared.locks.isEmpty { DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: { [weak self] in self?.scan() }) } @@ -102,12 +102,14 @@ final class NearbyLocksViewController: UITableViewController { #if targetEnvironment(macCatalyst) scan() #else - if Store.shared.lockManager.central.state == .poweredOn, - Store.shared.locks.value.isEmpty || Store.shared.lockInformation.value.isEmpty { - scan() - } else { - // Update beacon status - BeaconController.shared.scanBeacons() + Task { + if await Store.shared.central.state == .poweredOn, + Store.shared.locks.isEmpty || Store.shared.lockInformation.isEmpty { + scan() + } else { + // Update beacon status + BeaconController.shared.scanBeacons() + } } #endif } @@ -149,8 +151,8 @@ final class NearbyLocksViewController: UITableViewController { self.refreshControl?.endRefreshing() /// ignore if off or not authorized - guard LockManager.shared.central.state == .poweredOn - else { return } // cannot scan + //guard await Store.shared.central.state == .poweredOn + // else { return } // cannot scan userActivity = NSUserActivity(.screen(.nearbyLocks)) userActivity?.becomeCurrent() @@ -161,10 +163,10 @@ final class NearbyLocksViewController: UITableViewController { // scan performActivity({ try await Store.shared.scan() - for peripheral in Store.shared.peripherals.values { - do { try Store.shared.readInformation(peripheral) } + for peripheral in Store.shared.peripherals.keys { + do { try await Store.shared.readInformation(peripheral) } catch { - log("⚠️ Could not read information for peripheral \(peripheral.scanData.peripheral)") + log("⚠️ Could not read information for peripheral \(peripheral)") // try again mainQueue { [weak self] in self?.scan() } } @@ -202,7 +204,7 @@ final class NearbyLocksViewController: UITableViewController { override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) { let lock = self[indexPath] - guard let information = Store.shared.lockInformation.value[lock.scanData.peripheral] + guard let information = Store.shared.lockInformation[lock] else { assertionFailure(); return } view(lock: information.id) } @@ -216,8 +218,9 @@ final class NearbyLocksViewController: UITableViewController { private func configureView() { // sort by signal - self.items = Store.shared.peripherals.values - .sorted(by: { $0.scanData.rssi < $1.scanData.rssi }) + self.items = Store.shared.peripherals + .sorted(by: { $0.value.rssi < $1.value.rssi }) + .map { $0.key } } private func configure(cell: LockTableViewCell, at indexPath: IndexPath) { @@ -230,7 +233,7 @@ final class NearbyLocksViewController: UITableViewController { let isEnabled: Bool let showDetail: Bool - if let information = Store.shared.lockInformation.value[lock.scanData.peripheral] { + if let information = Store.shared.lockInformation[lock] { isEnabled = true @@ -293,7 +296,7 @@ final class NearbyLocksViewController: UITableViewController { private func select(_ lock: NativeCentral.Peripheral) { - guard let information = Store.shared.lockInformation[lock.scanData.peripheral] + guard let information = Store.shared.lockInformation[lock] else { return } let identifier = information.id diff --git a/iOS/Today/TodayViewController.swift b/iOS/Today/TodayViewController.swift index 38dcd9f8..3f0195e2 100644 --- a/iOS/Today/TodayViewController.swift +++ b/iOS/Today/TodayViewController.swift @@ -16,7 +16,6 @@ import DarwinGATT import CoreLock import LockKit import Combine -import Combine final class TodayViewController: UITableViewController { @@ -64,17 +63,17 @@ final class TodayViewController: UITableViewController { tableView.tableFooterView = UIView() // Set Logging - LockManager.shared.log = { log("🔒 LockManager: " + $0) } + Store.shared.central.log = { log("📲 Central: " + $0) } BeaconController.shared.log = { log("📶 \(BeaconController.self): " + $0) } // observe model changes - peripheralsObserver = Store.shared.peripherals.sink { [weak self] _ in + peripheralsObserver = Store.shared.$peripherals.sink { [weak self] _ in self?.locksChanged() } - informationObserver = Store.shared.lockInformation.sink { [weak self] _ in + informationObserver = Store.shared.$lockInformation.sink { [weak self] _ in self?.locksChanged() } - locksObserver = Store.shared.locks.sink { [weak self] _ in + locksObserver = Store.shared.$locks.sink { [weak self] _ in self?.locksChanged() } @@ -145,15 +144,15 @@ final class TodayViewController: UITableViewController { private func configureView() { - let locks = Store.shared.peripherals.value.values + let locks = Store.shared.peripherals.values .lazy - .sorted { $0.scanData.rssi < $1.scanData.rssi } + .sorted { $0.rssi < $1.rssi } .lazy - .compactMap { Store.shared.lockInformation.value[$0.scanData.peripheral] } + .compactMap { Store.shared.lockInformation[$0.peripheral] } .lazy .compactMap { information in Store.shared[lock: information.id] - .flatMap { (identifier: information.id, cache: $0) } + .flatMap { (id: information.id, cache: $0) } } if locks.isEmpty { @@ -174,9 +173,9 @@ final class TodayViewController: UITableViewController { BeaconController.shared.scanBeacons() // scan for devices - DispatchQueue.bluetooth.async { - defer { mainQueue { self.isScanning = false } } - do { try Store.shared.scan(duration: 1.0) } + Task { + defer { Task { await MainActor.run { self.isScanning = false } } } + do { try await Store.shared.scan(duration: 1.0) } catch { log("⚠️ Could not scan: \(error.localizedDescription)") mainQueue { completion?(false) } @@ -241,12 +240,12 @@ final class TodayViewController: UITableViewController { scan() case let .lock(identifier, cache): // unlock - DispatchQueue.bluetooth.async { + Task { log("Unlock \(cache.name) \(identifier)") do { guard let peripheral = Store.shared.device(for: identifier) else { assertionFailure("Peripheral not found"); return } - try Store.shared.unlock(peripheral) + try await Store.shared.unlock(peripheral) } catch { log("⚠️ Could not unlock: \(error.localizedDescription)") } diff --git a/iOS/Watch Extension/Controller/InterfaceController.swift b/iOS/Watch Extension/Controller/InterfaceController.swift index 4cf4c45b..7355c7eb 100644 --- a/iOS/Watch Extension/Controller/InterfaceController.swift +++ b/iOS/Watch Extension/Controller/InterfaceController.swift @@ -40,13 +40,13 @@ final class InterfaceController: WKInterfaceController { super.awake(withContext: context) // Observe changes - peripheralsObserver = Store.shared.peripherals.sink { [weak self] _ in + peripheralsObserver = Store.shared.$peripherals.sink { [weak self] _ in mainQueue { self?.configureView() } } - informationObserver = Store.shared.lockInformation.sink { [weak self] _ in + informationObserver = Store.shared.$lockInformation.sink { [weak self] _ in mainQueue { self?.configureView() } } - locksObserver = Store.shared.locks.sink { [weak self] _ in + locksObserver = Store.shared.$locks.sink { [weak self] _ in mainQueue { self?.configureView() } } @@ -58,7 +58,7 @@ final class InterfaceController: WKInterfaceController { // scan for locks DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in - if Store.shared.lockInformation.value.isEmpty { + if Store.shared.lockInformation.isEmpty { self?.scan() } } @@ -102,35 +102,38 @@ final class InterfaceController: WKInterfaceController { Store.shared.syncApp() } - /// ignore if off or not authorized - guard LockManager.shared.central.state == .poweredOn - else { return } // cannot scan - - activity.becomeCurrent() - - // scan - performActivity(queue: .bluetooth, { - try Store.shared.scan() - for peripheral in Store.shared.peripherals.value.values { - do { try Store.shared.readInformation(peripheral) } - catch { log("⚠️ Could not read information for peripheral \(peripheral.scanData.peripheral)") } + Task { + /// ignore if off or not authorized + guard await Store.shared.central.state == .poweredOn + else { return } // cannot scan + + await MainActor.run { + activity.becomeCurrent() } - }) + + // scan + performActivity({ + try await Store.shared.scan() + for peripheral in Store.shared.peripherals.keys { + do { try await Store.shared.readInformation(peripheral) } + catch { log("⚠️ Could not read information for peripheral \(peripheral)") } + } + }) + } + } private func configureView() { - self.items = Store.shared.peripherals.value.values + self.items = Store.shared.peripherals.values .lazy - .sorted { $0.scanData.rssi < $1.scanData.rssi } - .lazy - .compactMap { (device) in - Store.shared.lockInformation.value[device.scanData.peripheral] - .flatMap { (device, $0) } - } - .compactMap { (device, information) in - Store.shared[lock: information.id].flatMap { - Item(identifier: information.id, cache: $0, peripheral: device) + .sorted { $0.rssi < $1.rssi } + .map { $0.peripheral } + .compactMap { (peripheral) in + Store.shared.lockInformation[peripheral].flatMap { (information) in + Store.shared[lock: information.id].flatMap { (cache) in + Item(id: information.id, cache: cache, peripheral: peripheral) + } } } diff --git a/iOS/Watch Extension/ExtensionDelegate.swift b/iOS/Watch Extension/ExtensionDelegate.swift index 5959e64b..5320632f 100644 --- a/iOS/Watch Extension/ExtensionDelegate.swift +++ b/iOS/Watch Extension/ExtensionDelegate.swift @@ -29,7 +29,7 @@ final class ExtensionDelegate: NSObject, WKExtensionDelegate { log("⌚️ Launching SmartLock Watch v\(Bundle.InfoPlist.shortVersion) Build \(Bundle.InfoPlist.version)") // setup logging - Store.shared.lockManager.log = { log("🔒 LockManager: " + $0) } + Store.shared.central.log = { log("📲 Central: " + $0) } SessionController.shared.log = { log("📱 SessionController: " + $0) } // sync with iOS app on launch @@ -142,9 +142,9 @@ internal extension ExtensionDelegate { func scan(completion: (() -> ())? = nil) { // scan for locks - DispatchQueue.bluetooth.async { + Task { defer { mainQueue { completion?() } } - do { try Store.shared.scan() } + do { try await Store.shared.scan() } catch { log("⚠️ Unable to scan: \(error.localizedDescription)") } } } From 70991b2e1fb6e5a5d9c1d36a1cbced38e21a0ec7 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 21:31:30 -0700 Subject: [PATCH 039/229] [iOS] Fixed `JGProgressHUD` usage --- .gitignore | 1 + iOS/SmartLock.xcodeproj/project.pbxproj | 17 ++++------------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 5e5d0bbd..a43ac435 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ playground.xcworkspace # Carthage Carthage/* +iOS/JGProgressHUD # fastlane # diff --git a/iOS/SmartLock.xcodeproj/project.pbxproj b/iOS/SmartLock.xcodeproj/project.pbxproj index 84d3f426..ebcefa23 100644 --- a/iOS/SmartLock.xcodeproj/project.pbxproj +++ b/iOS/SmartLock.xcodeproj/project.pbxproj @@ -92,8 +92,9 @@ 6E7503C32318E78400103EBB /* WatchApplicationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7503C22318E78400103EBB /* WatchApplicationContext.swift */; }; 6E7503C42318E78400103EBB /* WatchApplicationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7503C22318E78400103EBB /* WatchApplicationContext.swift */; }; 6E7E787528D6CACD00B81B65 /* JGProgressHUD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E7E787228D6CAAB00B81B65 /* JGProgressHUD.framework */; }; - 6E7E787628D6CACD00B81B65 /* JGProgressHUD.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6E7E787228D6CAAB00B81B65 /* JGProgressHUD.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 6E7E787828D6D37E00B81B65 /* WatchIntent.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6ED8206C2316711900B69520 /* WatchIntent.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 6E7E787C28D6D63E00B81B65 /* JGProgressHUD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E7E787228D6CAAB00B81B65 /* JGProgressHUD.framework */; }; + 6E7E787D28D6D63E00B81B65 /* JGProgressHUD.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6E7E787228D6CAAB00B81B65 /* JGProgressHUD.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 6E806B8C230F4F2800C6FF78 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E806B8B230F4F2800C6FF78 /* Preferences.swift */; }; 6E806B94230F4F4700C6FF78 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E806B93230F4F4700C6FF78 /* Keychain.swift */; }; 6E806B96230F4F7300C6FF78 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E806B95230F4F7300C6FF78 /* AppGroup.swift */; }; @@ -619,17 +620,7 @@ files = ( 6E9C95FD23114D07007C18FE /* CoreLock.framework in Embed Frameworks */, 6E9C95FF23114D60007C18FE /* LockKit.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; - 6E7E787728D6CACD00B81B65 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 6E7E787628D6CACD00B81B65 /* JGProgressHUD.framework in Embed Frameworks */, + 6E7E787D28D6D63E00B81B65 /* JGProgressHUD.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -965,6 +956,7 @@ files = ( 6E9C95FC23114D07007C18FE /* CoreLock.framework in Frameworks */, 6E9C95FE23114D60007C18FE /* LockKit.framework in Frameworks */, + 6E7E787C28D6D63E00B81B65 /* JGProgressHUD.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1753,7 +1745,6 @@ 6E994A80230DFB6C0076DC1B /* Sources */, 6E994A81230DFB6C0076DC1B /* Frameworks */, 6E994A82230DFB6C0076DC1B /* Resources */, - 6E7E787728D6CACD00B81B65 /* Embed Frameworks */, ); buildRules = ( ); From 2f991d0a4b8b31b67f0cb97af44025c05823d95b Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 21:48:46 -0700 Subject: [PATCH 040/229] Fixed iOS crash --- iOS/LockKit/Model/Store.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/iOS/LockKit/Model/Store.swift b/iOS/LockKit/Model/Store.swift index e9438ee7..05718e5e 100644 --- a/iOS/LockKit/Model/Store.swift +++ b/iOS/LockKit/Model/Store.swift @@ -498,9 +498,7 @@ public extension Store { } func readInformation(_ lock: NativeCentral.Peripheral) async throws { - - assert(Thread.isMainThread == false) - + let information = try await central.readInformation( for: lock ) From feda9e07eee06a7c7214166ad7ad13765c7e9ad8 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 22:38:47 -0700 Subject: [PATCH 041/229] [lockd] Fixed lock information error --- Sources/CoreLockGATTServer/LockGATTController.swift | 2 +- Sources/CoreLockGATTServer/LockServiceController.swift | 2 +- Sources/lockd/LockDaemon.swift | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/CoreLockGATTServer/LockGATTController.swift b/Sources/CoreLockGATTServer/LockGATTController.swift index 5476cc24..49fa5f0f 100644 --- a/Sources/CoreLockGATTServer/LockGATTController.swift +++ b/Sources/CoreLockGATTServer/LockGATTController.swift @@ -53,7 +53,7 @@ public final class LockGATTController { public func setHardware(_ newValue: LockHardware) async { self.hardware = newValue await deviceInformationController.setHardware(newValue) - await deviceInformationController.setHardware(newValue) + await lockServiceController.setHardware(newValue) } private func willRead(_ request: GATTReadRequest) -> ATTError? { diff --git a/Sources/CoreLockGATTServer/LockServiceController.swift b/Sources/CoreLockGATTServer/LockServiceController.swift index 6984f7d8..48defab4 100644 --- a/Sources/CoreLockGATTServer/LockServiceController.swift +++ b/Sources/CoreLockGATTServer/LockServiceController.swift @@ -264,7 +264,7 @@ public final class LockGATTServiceController : G // MARK: - Private Methods - private func updateInformation() async { + public func updateInformation() async { let status: LockStatus = authorization.isEmpty ? .setup : .unlock let id = configurationStore.configuration.id diff --git a/Sources/lockd/LockDaemon.swift b/Sources/lockd/LockDaemon.swift index 13d772d9..8795c233 100644 --- a/Sources/lockd/LockDaemon.swift +++ b/Sources/lockd/LockDaemon.swift @@ -121,6 +121,7 @@ struct LockDaemon { controller?.lockServiceController.authorization = authorization controller?.lockServiceController.events = events controller?.lockServiceController.setupSecret = setupSecret.sharedSecret + await controller?.lockServiceController.updateInformation() /* // configure web server webServer.authorization = authorization From 29ba7b4e12277f91504dc0330bca50aa65fcd37d Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 22:39:52 -0700 Subject: [PATCH 042/229] [IOS] Add delay before searching for iBeacon --- iOS/LockKit/Model/Store.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/iOS/LockKit/Model/Store.swift b/iOS/LockKit/Model/Store.swift index 05718e5e..bee732dd 100644 --- a/iOS/LockKit/Model/Store.swift +++ b/iOS/LockKit/Model/Store.swift @@ -322,6 +322,7 @@ public final class Store: ObservableObject { do { guard let _ = try await self.device(for: beacon, scanDuration: 1.0) else { log("⚠️ Could not find lock \(beacon) for beacon \(beacon)") + try? await Task.sleep(nanoseconds: 10 * 1_000_000_000) self.beaconController.scanBeacon(for: beacon) return } From 1170a5393e5df1de3da945b3984f35a9450f7a19 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 23:45:53 -0700 Subject: [PATCH 043/229] [iOS] Disable writing log files --- iOS/LockKit/Model/Log.swift | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/iOS/LockKit/Model/Log.swift b/iOS/LockKit/Model/Log.swift index 81e3014d..edc9293e 100644 --- a/iOS/LockKit/Model/Log.swift +++ b/iOS/LockKit/Model/Log.swift @@ -18,25 +18,18 @@ public func log(_ text: String) { print(text) } #endif - - DispatchQueue.log.async { - - // only print for debug builds - #if DEBUG - print(text) - #endif - - let dateString = Log.dateFormatter.string(from: date) - - do { try Log.shared.log(dateString + " " + text) } - catch CocoaError.fileWriteNoPermission { - // unable to write due to permissions - if #available(iOS 13, *) { - assertionFailure("You don’t have permission to save the log file") - } - } - catch { assertionFailure("Could not write log: \(error)") } - } + /* + let dateString = Log.dateFormatter.string(from: date) + + do { try Log.shared.log(dateString + " " + text) } + catch CocoaError.fileWriteNoPermission { + // unable to write due to permissions + if #available(iOS 13, *) { + assertionFailure("You don’t have permission to save the log file") + } + } + catch { assertionFailure("Could not write log: \(error)") } + */ } fileprivate extension Log { From 154c8b9fbc8199e7708609ccda503e8ec5810538 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 17 Sep 2022 23:46:37 -0700 Subject: [PATCH 044/229] [iOS] Updated `BeaconController` for Swift 5.7 --- iOS/LockKit/Model/BeaconController.swift | 104 +++++------------------ 1 file changed, 23 insertions(+), 81 deletions(-) diff --git a/iOS/LockKit/Model/BeaconController.swift b/iOS/LockKit/Model/BeaconController.swift index 3f0faa90..c3bd900d 100644 --- a/iOS/LockKit/Model/BeaconController.swift +++ b/iOS/LockKit/Model/BeaconController.swift @@ -103,29 +103,13 @@ public final class BeaconController { locationManager.startMonitoring(for: region) } #endif - if #available(iOS 13, *) { - if locationManager.rangedBeaconConstraints.contains(.init(region)) == false { - locationManager.startRangingBeacons(satisfying: .init(region)) - } - } else { - #if !targetEnvironment(macCatalyst) - if locationManager.rangedRegions.contains(region) == false { - locationManager.startRangingBeacons(in: region) - } - #endif + if locationManager.rangedBeaconConstraints.contains(.init(region)) == false { + locationManager.startRangingBeacons(satisfying: .init(region)) } locationManager.requestState(for: region) case .authorizedWhenInUse: - if #available(iOS 13, *) { - if locationManager.rangedBeaconConstraints.contains(.init(region)) == false { - locationManager.startRangingBeacons(satisfying: .init(region)) - } - } else { - #if !targetEnvironment(macCatalyst) - if locationManager.rangedRegions.contains(region) == false { - locationManager.startRangingBeacons(in: region) - } - #endif + if locationManager.rangedBeaconConstraints.contains(.init(region)) == false { + locationManager.startRangingBeacons(satisfying: .init(region)) } locationManager.requestState(for: region) case .denied, @@ -136,16 +120,8 @@ public final class BeaconController { // try just in case, ignore errors locationManager.requestState(for: region) locationManager.startMonitoring(for: region) - if #available(iOS 13, *) { - if locationManager.rangedBeaconConstraints.contains(.init(region)) == false { - locationManager.startRangingBeacons(satisfying: .init(region)) - } - } else { - #if !targetEnvironment(macCatalyst) - if locationManager.rangedRegions.contains(region) == false { - locationManager.startRangingBeacons(in: region) - } - #endif + if locationManager.rangedBeaconConstraints.contains(.init(region)) == false { + locationManager.startRangingBeacons(satisfying: .init(region)) } } } @@ -195,23 +171,11 @@ private extension BeaconController { else { assertionFailure(); return } if let beaconRegion = region as? CLBeaconRegion { - - // start ranging beacons - if #available(iOS 13, *) { - if manager.rangedBeaconConstraints.contains(.init(beaconRegion)) == false { - manager.startRangingBeacons(satisfying: .init(beaconRegion)) - } - } else { - #if !targetEnvironment(macCatalyst) - if manager.rangedRegions.contains(region) == false { - manager.startRangingBeacons(in: beaconRegion.proximityUUID) - } - #endif + if manager.rangedBeaconConstraints.contains(.init(beaconRegion)) == false { + manager.startRangingBeacons(satisfying: .init(beaconRegion)) } - guard let beacon = beaconController.beacons.first(where: { $0.value.region == region })?.value - else { assertionFailure("Invalid beacon \(beaconRegion.proximityUUID)"); return } - + else { assertionFailure("Invalid beacon \(beaconRegion.uuid)"); return } // update state beacon.state = .inside } @@ -227,16 +191,8 @@ private extension BeaconController { if let beaconRegion = region as? CLBeaconRegion { // stop ranging beacons - if #available(iOS 13, *) { - if manager.rangedBeaconConstraints.contains(.init(beaconRegion)) { - manager.stopRangingBeacons(satisfying: .init(beaconRegion)) - } - } else { - #if !targetEnvironment(macCatalyst) - if manager.rangedRegions.contains(beaconRegion) { - manager.stopRangingBeacons(in: beaconRegion) - } - #endif + if manager.rangedBeaconConstraints.contains(.init(beaconRegion)) { + manager.stopRangingBeacons(satisfying: .init(beaconRegion)) } guard let beacon = beaconController.beacons.first(where: { $0.value.region == region })?.value @@ -254,26 +210,28 @@ private extension BeaconController { if let beaconRegion = region as? CLBeaconRegion { - let newState: Beacon.State + let newState: Beacon.State? switch state { case .unknown: // start ranging beacons - manager.startRangingBeacons(in: beaconRegion.proximityUUID) - newState = .outside + manager.startRangingBeacons(in: beaconRegion.uuid) + newState = nil case .inside: // start ranging beacons - manager.startRangingBeacons(in: beaconRegion.proximityUUID) + manager.startRangingBeacons(in: beaconRegion.uuid) newState = .inside case .outside: // stop ranging beacons - manager.stopRangingBeacons(in: beaconRegion.proximityUUID) + manager.stopRangingBeacons(in: beaconRegion.uuid) newState = .outside } guard let beacon = beaconController?.beacons.first(where: { $0.value.region == region })?.value else { return } - beacon.state = newState + if let state = newState { + beacon.state = state + } } } @@ -323,7 +281,7 @@ private extension BeaconController { @objc public func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) { - didRange(beacons: beacons, for: region.proximityUUID) + didRange(beacons: beacons, for: region.uuid) } @objc @@ -375,34 +333,18 @@ internal extension CLLocationManager { } func startRangingBeacons(in uuid: UUID) { - if #available(iOS 13, iOSApplicationExtension 13.0, *) { - startRangingBeacons(satisfying: .init(uuid: uuid)) - } else { - #if !targetEnvironment(macCatalyst) - startRangingBeacons(in: CLBeaconRegion(proximityUUID: uuid, identifier: uuid.uuidString)) - #endif - } + startRangingBeacons(satisfying: .init(uuid: uuid)) } func stopRangingBeacons(in uuid: UUID) { - if #available(iOS 13, iOSApplicationExtension 13.0, *) { - stopRangingBeacons(satisfying: .init(uuid: uuid)) - } else { - #if !targetEnvironment(macCatalyst) - stopRangingBeacons(in: CLBeaconRegion(proximityUUID: uuid, identifier: uuid.uuidString)) - #endif - } + stopRangingBeacons(satisfying: .init(uuid: uuid)) } } internal extension CLBeaconRegion { convenience init(uuid id: UUID) { - if #available(iOS 13.0, iOSApplicationExtension 13.0, *) { - self.init(beaconIdentityConstraint: .init(uuid: id), identifier: id.uuidString) - } else { - self.init(proximityUUID: id, identifier: id.uuidString) - } + self.init(beaconIdentityConstraint: .init(uuid: id), identifier: id.uuidString) } } From e5e662dc21b963f4ddffad52fae0ae9c0b1e97e2 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 00:37:47 -0700 Subject: [PATCH 045/229] [iOS] Updated `LockActivityHandlingViewController` --- iOS/LockKit/Model/ActivityHandling.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOS/LockKit/Model/ActivityHandling.swift b/iOS/LockKit/Model/ActivityHandling.swift index b04748fd..e0d49316 100644 --- a/iOS/LockKit/Model/ActivityHandling.swift +++ b/iOS/LockKit/Model/ActivityHandling.swift @@ -18,4 +18,4 @@ public protocol LockActivityHandling { // MARK: - View Controller -public protocol LockActivityHandlingViewController: class, LockActivityHandling { } +public protocol LockActivityHandlingViewController: AnyObject, LockActivityHandling { } From 9beecfa8604cc7914b8d7c1fbae8dd25e5e85ccd Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 02:26:34 -0700 Subject: [PATCH 046/229] [iOS] Added SwiftUI app --- Xcode/LockKit/LockKit.h | 18 + Xcode/LockKit/Model/Central.swift | 17 + Xcode/LockKit/Model/Store.swift | 144 +++ Xcode/MatterLock/Info.plist | 13 + Xcode/MatterLock/RequestHandler.swift | 25 + Xcode/SmartLock.xcodeproj/project.pbxproj | 821 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/swiftpm/Package.resolved | 68 ++ Xcode/SmartLock/App.swift | 21 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 63 ++ Xcode/SmartLock/Assets.xcassets/Contents.json | 6 + Xcode/SmartLock/Model/Persistence.swift | 57 ++ .../SmartLock.xcdatamodeld/.xccurrentversion | 8 + .../SmartLock.xcdatamodel/contents | 9 + .../Preview Assets.xcassets/Contents.json | 6 + Xcode/SmartLock/SmartLock.entitlements | 10 + Xcode/SmartLock/View/ContentView.swift | 90 ++ Xcode/SmartLock/View/NearbyDevicesView.swift | 49 ++ 20 files changed, 1451 insertions(+) create mode 100644 Xcode/LockKit/LockKit.h create mode 100644 Xcode/LockKit/Model/Central.swift create mode 100644 Xcode/LockKit/Model/Store.swift create mode 100644 Xcode/MatterLock/Info.plist create mode 100644 Xcode/MatterLock/RequestHandler.swift create mode 100644 Xcode/SmartLock.xcodeproj/project.pbxproj create mode 100644 Xcode/SmartLock.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 Xcode/SmartLock/App.swift create mode 100644 Xcode/SmartLock/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Xcode/SmartLock/Assets.xcassets/Contents.json create mode 100644 Xcode/SmartLock/Model/Persistence.swift create mode 100644 Xcode/SmartLock/Model/SmartLock.xcdatamodeld/.xccurrentversion create mode 100644 Xcode/SmartLock/Model/SmartLock.xcdatamodeld/SmartLock.xcdatamodel/contents create mode 100644 Xcode/SmartLock/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 Xcode/SmartLock/SmartLock.entitlements create mode 100644 Xcode/SmartLock/View/ContentView.swift create mode 100644 Xcode/SmartLock/View/NearbyDevicesView.swift diff --git a/Xcode/LockKit/LockKit.h b/Xcode/LockKit/LockKit.h new file mode 100644 index 00000000..71a5df0f --- /dev/null +++ b/Xcode/LockKit/LockKit.h @@ -0,0 +1,18 @@ +// +// LockKit.h +// LockKit +// +// Created by Alsey Coleman Miller on 9/18/22. +// + +#import + +//! Project version number for LockKit. +FOUNDATION_EXPORT double LockKitVersionNumber; + +//! Project version string for LockKit. +FOUNDATION_EXPORT const unsigned char LockKitVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Xcode/LockKit/Model/Central.swift b/Xcode/LockKit/Model/Central.swift new file mode 100644 index 00000000..f4d41c4a --- /dev/null +++ b/Xcode/LockKit/Model/Central.swift @@ -0,0 +1,17 @@ +// +// Central.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/18/22. +// + +#if canImport(CoreBluetooth) && canImport(DarwinGATT) +import Foundation +import CoreBluetooth +import Bluetooth +import GATT +import DarwinGATT + +public typealias NativeCentral = DarwinCentral +public typealias NativePeripheral = DarwinCentral.Peripheral +#endif diff --git a/Xcode/LockKit/Model/Store.swift b/Xcode/LockKit/Model/Store.swift new file mode 100644 index 00000000..b6ac2586 --- /dev/null +++ b/Xcode/LockKit/Model/Store.swift @@ -0,0 +1,144 @@ +// +// Store.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/18/22. +// + +import Foundation +import Combine +@_exported import Bluetooth +@_exported import GATT +import DarwinGATT +@_exported import CoreLock + +/// Lock Store object +@MainActor +public final class Store: ObservableObject { + + public static let shared = Store() + + // MARK: - Properties + + @Published + public internal(set) var isScanning = false + + @Published + public var peripherals = [NativePeripheral: ScanData]() + + @Published + public var lockInformation = [NativePeripheral: LockInformation]() + + public lazy var central = DarwinCentral() + + private var scanStream: AsyncCentralScan? + + // MARK: - Initialization + + private init() { + _ = central + } + +} + +// MARK: - Bluetooth Methods + +public extension Store { + + func scan() async { + isScanning = true + let filterDuplicates = true //preferences.filterDuplicates + self.peripherals.removeAll(keepingCapacity: true) + let stream = central.scan( + with: [LockService.uuid], + filterDuplicates: filterDuplicates + ) + self.scanStream = stream + Task { + do { + for try await scanData in stream { + guard let serviceUUIDs = scanData.advertisementData.serviceUUIDs, + serviceUUIDs.contains(LockService.uuid) + else { continue } + // cache found device + self.peripherals[scanData.peripheral] = scanData + } + } catch { + print("Scanning error \(error)") + } + isScanning = false + } + } + + func scan(duration: TimeInterval) async { + await scan() + Task { + try? await Task.sleep(nanoseconds: UInt64(duration) * 1_000_000_000) + stopScanning() + } + } + + func stopScanning() { + scanStream?.stop() + scanStream = nil + isScanning = false + } + + func readInformation(_ lock: DarwinCentral.Peripheral) async throws { + + let information = try await central.readInformation( + for: lock + ) + + // update lock information cache + self.lockInformation[lock] = information + //self[lock: information.id]?.information = LockCache.Information(information) + } + + /// Setup a lock. + func setup( + _ lock: DarwinCentral.Peripheral, + using sharedSecret: KeyData, + name: String + ) async throws { + + let setupRequest = SetupRequest() + let information = try await central.setup( + setupRequest, + using: sharedSecret, + for: lock + ) + + let ownerKey = Key(setup: setupRequest) + /* + let lockCache = LockCache( + key: ownerKey, + name: name, + information: .init(information) + ) + + // store key + self[lock: information.id] = lockCache + self[key: ownerKey.id] = setupRequest.secret + */ + // update lock information + self.lockInformation[lock] = information + } + + func unlock( + _ lock: DarwinCentral.Peripheral, + action: UnlockAction = .default + ) async throws { + /* + // get lock key + guard let key = self.key(for: lock) + else { return false } + + try await central.unlock( + action, + using: key, + for: lock + ) + */ + } +} diff --git a/Xcode/MatterLock/Info.plist b/Xcode/MatterLock/Info.plist new file mode 100644 index 00000000..ea2b5094 --- /dev/null +++ b/Xcode/MatterLock/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.HomeKit.extension.Matter + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).RequestHandler + + + diff --git a/Xcode/MatterLock/RequestHandler.swift b/Xcode/MatterLock/RequestHandler.swift new file mode 100644 index 00000000..43af592c --- /dev/null +++ b/Xcode/MatterLock/RequestHandler.swift @@ -0,0 +1,25 @@ +// +// RequestHandler.swift +// MatterLock +// +// Created by Alsey Coleman Miller on 9/18/22. +// + +import HomeKit + +class RequestHandler: HMMatterRequestHandler { + + override func rooms(in home: HMMatterHome) async throws -> [HMMatterRoom] { + // Use this function to return the rooms your ecosystem manages. + // If your ecosystem manages multiple homes, ensure you are returning rooms that belong to the provided home. + return [] + } + + override func pairAccessory(in home: HMMatterHome, onboardingPayload: String) async throws -> Void { + // Use this function to pair the accessory with your own CHIP/Matter stack + } + + override func configureAccessory(named accessoryName: String, room accessoryRoom: HMMatterRoom) async throws -> Void { + // Use this function to configure the (now) paired accessory with the given name and room. + } +} diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj new file mode 100644 index 00000000..8e02dd61 --- /dev/null +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -0,0 +1,821 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276BB28D708A000AF171B /* CoreLock */; }; + 6E3276C328D70A3700AF171B /* HomeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E3276C228D70A3700AF171B /* HomeKit.framework */; }; + 6E3276C628D70A3700AF171B /* RequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276C528D70A3700AF171B /* RequestHandler.swift */; }; + 6E3276CA28D70A3700AF171B /* MatterLock.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6E3276C128D70A3700AF171B /* MatterLock.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 6E3276D028D70C9400AF171B /* NearbyDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276CF28D70C9400AF171B /* NearbyDevicesView.swift */; }; + 6E3276D228D70CE100AF171B /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276D128D70CE100AF171B /* Store.swift */; }; + 6E3276D728D70FA000AF171B /* DarwinGATT in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276D628D70FA000AF171B /* DarwinGATT */; }; + 6E3276D928D70FA000AF171B /* GATT in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276D828D70FA000AF171B /* GATT */; }; + 6E3276DC28D7195400AF171B /* Central.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276DB28D7195400AF171B /* Central.swift */; }; + 6EA7768528D7061600018FA3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA7768428D7061600018FA3 /* App.swift */; }; + 6EA7768728D7061600018FA3 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA7768628D7061600018FA3 /* Persistence.swift */; }; + 6EA7768A28D7061600018FA3 /* SmartLock.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 6EA7768828D7061600018FA3 /* SmartLock.xcdatamodeld */; }; + 6EA7768C28D7061600018FA3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA7768B28D7061600018FA3 /* ContentView.swift */; }; + 6EA7768E28D7061600018FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6EA7768D28D7061600018FA3 /* Assets.xcassets */; }; + 6EA7769228D7061600018FA3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6EA7769128D7061600018FA3 /* Preview Assets.xcassets */; }; + 6EA776A028D707FE00018FA3 /* LockKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 6EA7769F28D707FE00018FA3 /* LockKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6EA776A328D707FE00018FA3 /* LockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; }; + 6EA776A428D707FE00018FA3 /* LockKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 6E3276C828D70A3700AF171B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6EA7767928D7061600018FA3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6E3276C028D70A3700AF171B; + remoteInfo = MatterLock; + }; + 6EA776A128D707FE00018FA3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6EA7767928D7061600018FA3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6EA7769C28D707FE00018FA3; + remoteInfo = LockKit; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 6E3276CE28D70A3700AF171B /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 6E3276CA28D70A3700AF171B /* MatterLock.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + 6EA776A828D707FE00018FA3 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 6EA776A428D707FE00018FA3 /* LockKit.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 6E3276BA28D7088D00AF171B /* SmartLock */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SmartLock; path = ..; sourceTree = ""; }; + 6E3276C128D70A3700AF171B /* MatterLock.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MatterLock.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 6E3276C228D70A3700AF171B /* HomeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HomeKit.framework; path = Library/Frameworks/HomeKit.framework; sourceTree = DEVELOPER_DIR; }; + 6E3276C528D70A3700AF171B /* RequestHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestHandler.swift; sourceTree = ""; }; + 6E3276C728D70A3700AF171B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 6E3276CF28D70C9400AF171B /* NearbyDevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyDevicesView.swift; sourceTree = ""; }; + 6E3276D128D70CE100AF171B /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; + 6E3276DB28D7195400AF171B /* Central.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Central.swift; sourceTree = ""; }; + 6EA7768128D7061600018FA3 /* SmartLock.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SmartLock.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 6EA7768428D7061600018FA3 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; + 6EA7768628D7061600018FA3 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; + 6EA7768928D7061600018FA3 /* SmartLock.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SmartLock.xcdatamodel; sourceTree = ""; }; + 6EA7768B28D7061600018FA3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 6EA7768D28D7061600018FA3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 6EA7768F28D7061600018FA3 /* SmartLock.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SmartLock.entitlements; sourceTree = ""; }; + 6EA7769128D7061600018FA3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 6EA7769D28D707FE00018FA3 /* LockKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LockKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6EA7769F28D707FE00018FA3 /* LockKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LockKit.h; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 6E3276BE28D70A3700AF171B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E3276C328D70A3700AF171B /* HomeKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6EA7767E28D7061600018FA3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6EA776A328D707FE00018FA3 /* LockKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6EA7769A28D707FE00018FA3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E3276D928D70FA000AF171B /* GATT in Frameworks */, + 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */, + 6E3276D728D70FA000AF171B /* DarwinGATT in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 6E3276B928D7088D00AF171B /* Packages */ = { + isa = PBXGroup; + children = ( + 6E3276BA28D7088D00AF171B /* SmartLock */, + ); + name = Packages; + sourceTree = ""; + }; + 6E3276C428D70A3700AF171B /* MatterLock */ = { + isa = PBXGroup; + children = ( + 6E3276C528D70A3700AF171B /* RequestHandler.swift */, + 6E3276C728D70A3700AF171B /* Info.plist */, + ); + path = MatterLock; + sourceTree = ""; + }; + 6E3276D328D70D6900AF171B /* View */ = { + isa = PBXGroup; + children = ( + 6EA7768B28D7061600018FA3 /* ContentView.swift */, + 6E3276CF28D70C9400AF171B /* NearbyDevicesView.swift */, + ); + path = View; + sourceTree = ""; + }; + 6E3276D428D70D7500AF171B /* Model */ = { + isa = PBXGroup; + children = ( + 6EA7768828D7061600018FA3 /* SmartLock.xcdatamodeld */, + 6EA7768628D7061600018FA3 /* Persistence.swift */, + ); + path = Model; + sourceTree = ""; + }; + 6E3276DA28D7136400AF171B /* Model */ = { + isa = PBXGroup; + children = ( + 6E3276D128D70CE100AF171B /* Store.swift */, + 6E3276DB28D7195400AF171B /* Central.swift */, + ); + path = Model; + sourceTree = ""; + }; + 6EA7767828D7061600018FA3 = { + isa = PBXGroup; + children = ( + 6E3276B928D7088D00AF171B /* Packages */, + 6EA7768328D7061600018FA3 /* SmartLock */, + 6EA7769E28D707FE00018FA3 /* LockKit */, + 6E3276C428D70A3700AF171B /* MatterLock */, + 6EA7768228D7061600018FA3 /* Products */, + 6EA776A928D7082300018FA3 /* Frameworks */, + ); + sourceTree = ""; + }; + 6EA7768228D7061600018FA3 /* Products */ = { + isa = PBXGroup; + children = ( + 6EA7768128D7061600018FA3 /* SmartLock.app */, + 6EA7769D28D707FE00018FA3 /* LockKit.framework */, + 6E3276C128D70A3700AF171B /* MatterLock.appex */, + ); + name = Products; + sourceTree = ""; + }; + 6EA7768328D7061600018FA3 /* SmartLock */ = { + isa = PBXGroup; + children = ( + 6EA7768428D7061600018FA3 /* App.swift */, + 6E3276D428D70D7500AF171B /* Model */, + 6E3276D328D70D6900AF171B /* View */, + 6EA7768D28D7061600018FA3 /* Assets.xcassets */, + 6EA7768F28D7061600018FA3 /* SmartLock.entitlements */, + 6EA7769028D7061600018FA3 /* Preview Content */, + ); + path = SmartLock; + sourceTree = ""; + }; + 6EA7769028D7061600018FA3 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 6EA7769128D7061600018FA3 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 6EA7769E28D707FE00018FA3 /* LockKit */ = { + isa = PBXGroup; + children = ( + 6EA7769F28D707FE00018FA3 /* LockKit.h */, + 6E3276DA28D7136400AF171B /* Model */, + ); + path = LockKit; + sourceTree = ""; + }; + 6EA776A928D7082300018FA3 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 6E3276C228D70A3700AF171B /* HomeKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 6EA7769828D707FE00018FA3 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 6EA776A028D707FE00018FA3 /* LockKit.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 6E3276C028D70A3700AF171B /* MatterLock */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6E3276CB28D70A3700AF171B /* Build configuration list for PBXNativeTarget "MatterLock" */; + buildPhases = ( + 6E3276BD28D70A3700AF171B /* Sources */, + 6E3276BE28D70A3700AF171B /* Frameworks */, + 6E3276BF28D70A3700AF171B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MatterLock; + productName = MatterLock; + productReference = 6E3276C128D70A3700AF171B /* MatterLock.appex */; + productType = "com.apple.product-type.app-extension"; + }; + 6EA7768028D7061600018FA3 /* SmartLock */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6EA7769528D7061600018FA3 /* Build configuration list for PBXNativeTarget "SmartLock" */; + buildPhases = ( + 6EA7767D28D7061600018FA3 /* Sources */, + 6EA7767E28D7061600018FA3 /* Frameworks */, + 6EA7767F28D7061600018FA3 /* Resources */, + 6EA776A828D707FE00018FA3 /* Embed Frameworks */, + 6E3276CE28D70A3700AF171B /* Embed Foundation Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 6EA776A228D707FE00018FA3 /* PBXTargetDependency */, + 6E3276C928D70A3700AF171B /* PBXTargetDependency */, + ); + name = SmartLock; + productName = SmartLock; + productReference = 6EA7768128D7061600018FA3 /* SmartLock.app */; + productType = "com.apple.product-type.application"; + }; + 6EA7769C28D707FE00018FA3 /* LockKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6EA776A528D707FE00018FA3 /* Build configuration list for PBXNativeTarget "LockKit" */; + buildPhases = ( + 6EA7769828D707FE00018FA3 /* Headers */, + 6EA7769928D707FE00018FA3 /* Sources */, + 6EA7769A28D707FE00018FA3 /* Frameworks */, + 6EA7769B28D707FE00018FA3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = LockKit; + packageProductDependencies = ( + 6E3276BB28D708A000AF171B /* CoreLock */, + 6E3276D628D70FA000AF171B /* DarwinGATT */, + 6E3276D828D70FA000AF171B /* GATT */, + ); + productName = LockKit; + productReference = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 6EA7767928D7061600018FA3 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1400; + LastUpgradeCheck = 1400; + TargetAttributes = { + 6E3276C028D70A3700AF171B = { + CreatedOnToolsVersion = 14.0; + }; + 6EA7768028D7061600018FA3 = { + CreatedOnToolsVersion = 14.0; + }; + 6EA7769C28D707FE00018FA3 = { + CreatedOnToolsVersion = 14.0; + LastSwiftMigration = 1400; + }; + }; + }; + buildConfigurationList = 6EA7767C28D7061600018FA3 /* Build configuration list for PBXProject "SmartLock" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 6EA7767828D7061600018FA3; + packageReferences = ( + 6E3276D528D70FA000AF171B /* XCRemoteSwiftPackageReference "GATT" */, + ); + productRefGroup = 6EA7768228D7061600018FA3 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 6EA7768028D7061600018FA3 /* SmartLock */, + 6EA7769C28D707FE00018FA3 /* LockKit */, + 6E3276C028D70A3700AF171B /* MatterLock */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 6E3276BF28D70A3700AF171B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6EA7767F28D7061600018FA3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6EA7769228D7061600018FA3 /* Preview Assets.xcassets in Resources */, + 6EA7768E28D7061600018FA3 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6EA7769B28D707FE00018FA3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 6E3276BD28D70A3700AF171B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E3276C628D70A3700AF171B /* RequestHandler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6EA7767D28D7061600018FA3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6EA7768528D7061600018FA3 /* App.swift in Sources */, + 6EA7768A28D7061600018FA3 /* SmartLock.xcdatamodeld in Sources */, + 6EA7768C28D7061600018FA3 /* ContentView.swift in Sources */, + 6E3276D028D70C9400AF171B /* NearbyDevicesView.swift in Sources */, + 6EA7768728D7061600018FA3 /* Persistence.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6EA7769928D707FE00018FA3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E3276D228D70CE100AF171B /* Store.swift in Sources */, + 6E3276DC28D7195400AF171B /* Central.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 6E3276C928D70A3700AF171B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6E3276C028D70A3700AF171B /* MatterLock */; + targetProxy = 6E3276C828D70A3700AF171B /* PBXContainerItemProxy */; + }; + 6EA776A228D707FE00018FA3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6EA7769C28D707FE00018FA3 /* LockKit */; + targetProxy = 6EA776A128D707FE00018FA3 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 6E3276CC28D70A3700AF171B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4W79SG34MW; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MatterLock/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = MatterLock; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock.MatterLock; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6E3276CD28D70A3700AF171B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4W79SG34MW; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MatterLock/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = MatterLock; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock.MatterLock; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 6EA7769328D7061600018FA3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TVOS_DEPLOYMENT_TARGET = 15.0; + WATCHOS_DEPLOYMENT_TARGET = 8.0; + }; + name = Debug; + }; + 6EA7769428D7061600018FA3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TVOS_DEPLOYMENT_TARGET = 15.0; + WATCHOS_DEPLOYMENT_TARGET = 8.0; + }; + name = Release; + }; + 6EA7769628D7061600018FA3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = SmartLock/SmartLock.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"SmartLock/Preview Content\""; + DEVELOPMENT_TEAM = 4W79SG34MW; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6EA7769728D7061600018FA3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = SmartLock/SmartLock.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"SmartLock/Preview Content\""; + DEVELOPMENT_TEAM = 4W79SG34MW; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.SmartLock; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 6EA776A628D707FE00018FA3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 4W79SG34MW; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.LockKit; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = auto; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 6EA776A728D707FE00018FA3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 4W79SG34MW; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.colemancda.LockKit; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = auto; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 6E3276CB28D70A3700AF171B /* Build configuration list for PBXNativeTarget "MatterLock" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6E3276CC28D70A3700AF171B /* Debug */, + 6E3276CD28D70A3700AF171B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6EA7767C28D7061600018FA3 /* Build configuration list for PBXProject "SmartLock" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6EA7769328D7061600018FA3 /* Debug */, + 6EA7769428D7061600018FA3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6EA7769528D7061600018FA3 /* Build configuration list for PBXNativeTarget "SmartLock" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6EA7769628D7061600018FA3 /* Debug */, + 6EA7769728D7061600018FA3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6EA776A528D707FE00018FA3 /* Build configuration list for PBXNativeTarget "LockKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6EA776A628D707FE00018FA3 /* Debug */, + 6EA776A728D707FE00018FA3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 6E3276D528D70FA000AF171B /* XCRemoteSwiftPackageReference "GATT" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/PureSwift/GATT.git"; + requirement = { + branch = master; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 6E3276BB28D708A000AF171B /* CoreLock */ = { + isa = XCSwiftPackageProductDependency; + productName = CoreLock; + }; + 6E3276D628D70FA000AF171B /* DarwinGATT */ = { + isa = XCSwiftPackageProductDependency; + package = 6E3276D528D70FA000AF171B /* XCRemoteSwiftPackageReference "GATT" */; + productName = DarwinGATT; + }; + 6E3276D828D70FA000AF171B /* GATT */ = { + isa = XCSwiftPackageProductDependency; + package = 6E3276D528D70FA000AF171B /* XCRemoteSwiftPackageReference "GATT" */; + productName = GATT; + }; +/* End XCSwiftPackageProductDependency section */ + +/* Begin XCVersionGroup section */ + 6EA7768828D7061600018FA3 /* SmartLock.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + 6EA7768928D7061600018FA3 /* SmartLock.xcdatamodel */, + ); + currentVersion = 6EA7768928D7061600018FA3 /* SmartLock.xcdatamodel */; + path = SmartLock.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ + }; + rootObject = 6EA7767928D7061600018FA3 /* Project object */; +} diff --git a/Xcode/SmartLock.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Xcode/SmartLock.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/Xcode/SmartLock.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..5f7f506a --- /dev/null +++ b/Xcode/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,68 @@ +{ + "pins" : [ + { + "identity" : "bluetooth", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/Bluetooth.git", + "state" : { + "revision" : "f28f73c1ff5e0877c212300ab356997e7e9570fa", + "version" : "6.1.0" + } + }, + { + "identity" : "bluetoothlinux", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/BluetoothLinux.git", + "state" : { + "branch" : "master", + "revision" : "ddd5493bd5382ca0132b8ca121dd35c7554ab7d0" + } + }, + { + "identity" : "gatt", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/GATT.git", + "state" : { + "branch" : "master", + "revision" : "579ffd583f9a32a88e68b69e12eb85856ee165cb" + } + }, + { + "identity" : "socket", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/Socket.git", + "state" : { + "branch" : "main", + "revision" : "c5d21b3b37a0fb55abee4c754e96ef552862d8a3" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/swift-system", + "state" : { + "branch" : "feature/dynamic-lib", + "revision" : "3e5be49e7cee5ba46f5b5bc994f08061dd9fe92a" + } + }, + { + "identity" : "swiftygpio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/uraimo/SwiftyGPIO.git", + "state" : { + "branch" : "master", + "revision" : "1754dc31e4d648c6999b3e664d8669d1a87c22eb" + } + }, + { + "identity" : "tlvcoding", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PureSwift/TLVCoding.git", + "state" : { + "branch" : "master", + "revision" : "49eae2e68320dbe1533eb2880ee82f7b6144517a" + } + } + ], + "version" : 2 +} diff --git a/Xcode/SmartLock/App.swift b/Xcode/SmartLock/App.swift new file mode 100644 index 00000000..29b4c24b --- /dev/null +++ b/Xcode/SmartLock/App.swift @@ -0,0 +1,21 @@ +// +// SmartLockApp.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/18/22. +// + +import SwiftUI + +@main +struct LockApp: App { + + //let persistenceController = PersistenceController.shared + + var body: some Scene { + WindowGroup { + NearbyDevicesView() + //.environment(\.managedObjectContext, persistenceController.container.viewContext) + } + } +} diff --git a/Xcode/SmartLock/Assets.xcassets/AccentColor.colorset/Contents.json b/Xcode/SmartLock/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/Xcode/SmartLock/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/Contents.json b/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..532cd729 --- /dev/null +++ b/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,63 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Xcode/SmartLock/Assets.xcassets/Contents.json b/Xcode/SmartLock/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Xcode/SmartLock/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Xcode/SmartLock/Model/Persistence.swift b/Xcode/SmartLock/Model/Persistence.swift new file mode 100644 index 00000000..0c2a6efb --- /dev/null +++ b/Xcode/SmartLock/Model/Persistence.swift @@ -0,0 +1,57 @@ +// +// Persistence.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/18/22. +// + +import CoreData + +struct PersistenceController { + + static let shared = PersistenceController() + + static var preview: PersistenceController = { + let result = PersistenceController(inMemory: true) + let viewContext = result.container.viewContext + for _ in 0..<10 { + let newItem = Item(context: viewContext) + newItem.timestamp = Date() + } + do { + try viewContext.save() + } catch { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + return result + }() + + let container: NSPersistentContainer + + init(inMemory: Bool = false) { + container = NSPersistentContainer(name: "SmartLock") + if inMemory { + container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") + } + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + + /* + Typical reasons for an error here include: + * The parent directory does not exist, cannot be created, or disallows writing. + * The persistent store is not accessible, due to permissions or data protection when the device is locked. + * The device is out of space. + * The store could not be migrated to the current model version. + Check the error message to determine what the actual problem was. + */ + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + container.viewContext.automaticallyMergesChangesFromParent = true + } +} diff --git a/Xcode/SmartLock/Model/SmartLock.xcdatamodeld/.xccurrentversion b/Xcode/SmartLock/Model/SmartLock.xcdatamodeld/.xccurrentversion new file mode 100644 index 00000000..0a618e66 --- /dev/null +++ b/Xcode/SmartLock/Model/SmartLock.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + SmartLock.xcdatamodel + + diff --git a/Xcode/SmartLock/Model/SmartLock.xcdatamodeld/SmartLock.xcdatamodel/contents b/Xcode/SmartLock/Model/SmartLock.xcdatamodeld/SmartLock.xcdatamodel/contents new file mode 100644 index 00000000..9ed2921a --- /dev/null +++ b/Xcode/SmartLock/Model/SmartLock.xcdatamodeld/SmartLock.xcdatamodel/contents @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Xcode/SmartLock/Preview Content/Preview Assets.xcassets/Contents.json b/Xcode/SmartLock/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Xcode/SmartLock/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Xcode/SmartLock/SmartLock.entitlements b/Xcode/SmartLock/SmartLock.entitlements new file mode 100644 index 00000000..f2ef3ae0 --- /dev/null +++ b/Xcode/SmartLock/SmartLock.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/Xcode/SmartLock/View/ContentView.swift b/Xcode/SmartLock/View/ContentView.swift new file mode 100644 index 00000000..34f2b9e5 --- /dev/null +++ b/Xcode/SmartLock/View/ContentView.swift @@ -0,0 +1,90 @@ +// +// ContentView.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/18/22. +// + +import SwiftUI +import CoreData + +struct ContentView: View { + @Environment(\.managedObjectContext) private var viewContext + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)], + animation: .default) + private var items: FetchedResults + + var body: some View { + NavigationView { + List { + ForEach(items) { item in + NavigationLink { + Text("Item at \(item.timestamp!, formatter: itemFormatter)") + } label: { + Text(item.timestamp!, formatter: itemFormatter) + } + } + .onDelete(perform: deleteItems) + } + .toolbar { +#if os(iOS) + ToolbarItem(placement: .navigationBarTrailing) { + EditButton() + } +#endif + ToolbarItem { + Button(action: addItem) { + Label("Add Item", systemImage: "plus") + } + } + } + Text("Select an item") + } + } + + private func addItem() { + withAnimation { + let newItem = Item(context: viewContext) + newItem.timestamp = Date() + + do { + try viewContext.save() + } catch { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } + } + + private func deleteItems(offsets: IndexSet) { + withAnimation { + offsets.map { items[$0] }.forEach(viewContext.delete) + + do { + try viewContext.save() + } catch { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } + } +} + +private let itemFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .medium + return formatter +}() + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift new file mode 100644 index 00000000..956597a2 --- /dev/null +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -0,0 +1,49 @@ +// +// NearbyDevicesView.swift +// SmartLock +// +// Created by Alsey Coleman Miller on 9/18/22. +// + +import SwiftUI +import LockKit + +struct NearbyDevicesView: View { + + @ObservedObject + var store: Store = .shared + + var body: some View { + ScrollView { + LazyVStack(alignment: .leading) { + ForEach(peripherals, id: \.id) { + Text(verbatim: $0.id.description) + } + } + } + .task { + await store.scan() + } + .onDisappear { + Task { + store.stopScanning() + } + } + } +} + +extension NearbyDevicesView { + + var peripherals: [NativePeripheral] { + store.peripherals + .lazy + .sorted(by: { $0.value.rssi < $1.value.rssi }) + .map { $0.key } + } +} + +struct NearbyDevicesView_Previews: PreviewProvider { + static var previews: some View { + NearbyDevicesView() + } +} From 50a721d13b80a750f7aa731e1ca7f2d16ac7ca8f Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 11:36:31 -0700 Subject: [PATCH 047/229] [App] Fixed macOS support for `PermissionIconView` --- Xcode/LockKit/View/AppKit/NSStyleKit.swift | 2771 +++++++++++++++++ .../AppKit/PermissionIconViewNSView.swift | 74 + Xcode/LockKit/View/PermissionIconView.swift | 32 + .../View/UIKit/PermissionIconViewUIView.swift | 106 + .../LockKit/View/UIKit/UIStyleKit.swift | 3 +- Xcode/SmartLock.xcodeproj/project.pbxproj | 107 +- Xcode/SmartLock/Model/Persistence.swift | 57 - .../SmartLock.xcdatamodeld/.xccurrentversion | 5 +- Xcode/SmartLock/View/ContentView.swift | 90 - Xcode/SmartLock/View/NearbyDevicesView.swift | 31 +- iOS/LockKit/View/PermissionIconView.swift | 77 - 11 files changed, 3065 insertions(+), 288 deletions(-) create mode 100644 Xcode/LockKit/View/AppKit/NSStyleKit.swift create mode 100644 Xcode/LockKit/View/AppKit/PermissionIconViewNSView.swift create mode 100644 Xcode/LockKit/View/PermissionIconView.swift create mode 100644 Xcode/LockKit/View/UIKit/PermissionIconViewUIView.swift rename iOS/LockKit/View/StyleKit.swift => Xcode/LockKit/View/UIKit/UIStyleKit.swift (99%) delete mode 100644 Xcode/SmartLock/Model/Persistence.swift delete mode 100644 Xcode/SmartLock/View/ContentView.swift delete mode 100644 iOS/LockKit/View/PermissionIconView.swift diff --git a/Xcode/LockKit/View/AppKit/NSStyleKit.swift b/Xcode/LockKit/View/AppKit/NSStyleKit.swift new file mode 100644 index 00000000..c406f853 --- /dev/null +++ b/Xcode/LockKit/View/AppKit/NSStyleKit.swift @@ -0,0 +1,2771 @@ +// +// StyleKit.swift +// Lock +// +// Created by Alsey Coleman Miller on 9/18/22. +// Copyright © 2022 ColemanCDA. All rights reserved. +// +// Generated by PaintCode +// http://www.paintcodeapp.com +// + + +#if canImport(AppKit) +import Cocoa + +public class StyleKit : NSObject { + + //// Cache + + private struct Cache { + static let bluetoothBlue: NSColor = NSColor(red: 0.035, green: 0.294, blue: 0.596, alpha: 1) + static let bluetoothDisabledGrey: NSColor = NSColor(red: 0.746, green: 0.733, blue: 0.733, alpha: 1) + static let wirelessBlue: NSColor = NSColor(red: 0.278, green: 0.506, blue: 0.976, alpha: 1) + static var imageOfActivityNewKey: NSImage? + } + + //// Colors + + @objc dynamic public class var bluetoothBlue: NSColor { return Cache.bluetoothBlue } + @objc dynamic public class var bluetoothDisabledGrey: NSColor { return Cache.bluetoothDisabledGrey } + @objc dynamic public class var wirelessBlue: NSColor { return Cache.wirelessBlue } + + //// Drawing Methods + + @objc dynamic public class func drawScan2(frame: NSRect = NSRect(x: 0, y: 0, width: 230, height: 230)) { + + //// Bezier Drawing + let bezierPath = NSBezierPath() + bezierPath.move(to: NSPoint(x: frame.minX + 0.43125 * frame.width, y: frame.minY + 0.17500 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.10625 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.56875 * frame.width, y: frame.minY + 0.17500 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.20625 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.55000 * frame.width, y: frame.minY + 0.19375 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.52500 * frame.width, y: frame.minY + 0.20625 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.43125 * frame.width, y: frame.minY + 0.17500 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.47500 * frame.width, y: frame.minY + 0.20625 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.44375 * frame.width, y: frame.minY + 0.19375 * frame.height)) + bezierPath.close() + StyleKit.wirelessBlue.setFill() + bezierPath.fill() + + + //// Bezier 4 Drawing + let bezier4Path = NSBezierPath() + bezier4Path.move(to: NSPoint(x: frame.minX + 0.28750 * frame.width, y: frame.minY + 0.31875 * frame.height)) + bezier4Path.line(to: NSPoint(x: frame.minX + 0.35625 * frame.width, y: frame.minY + 0.25000 * frame.height)) + bezier4Path.curve(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.30625 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.39375 * frame.width, y: frame.minY + 0.28750 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.44375 * frame.width, y: frame.minY + 0.30625 * frame.height)) + bezier4Path.curve(to: NSPoint(x: frame.minX + 0.64375 * frame.width, y: frame.minY + 0.25000 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.55625 * frame.width, y: frame.minY + 0.30625 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.60625 * frame.width, y: frame.minY + 0.28125 * frame.height)) + bezier4Path.line(to: NSPoint(x: frame.minX + 0.71250 * frame.width, y: frame.minY + 0.31875 * frame.height)) + bezier4Path.curve(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.40625 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.66250 * frame.width, y: frame.minY + 0.37500 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.58125 * frame.width, y: frame.minY + 0.40625 * frame.height)) + bezier4Path.curve(to: NSPoint(x: frame.minX + 0.28750 * frame.width, y: frame.minY + 0.31875 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.41875 * frame.width, y: frame.minY + 0.40625 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.33750 * frame.width, y: frame.minY + 0.37500 * frame.height)) + bezier4Path.close() + StyleKit.wirelessBlue.setFill() + bezier4Path.fill() + } + + @objc dynamic public class func drawBluetoothLogo(frame: NSRect = NSRect(x: 0, y: 0, width: 230, height: 230)) { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Group + NSGraphicsContext.saveGraphicsState() + context.translateBy(x: frame.minX + 0.13043 * frame.width, y: frame.minY + 0.99565 * frame.height) + context.scaleBy(x: 3.7, y: 3.7) + + + + //// Page-1 + //// Bluetooth_Smart_Logo + //// Shape Drawing + let shapePath = NSBezierPath() + shapePath.move(to: NSPoint(x: 25.55, y: -15.31)) + shapePath.line(to: NSPoint(x: 30.61, y: -20.37)) + shapePath.line(to: NSPoint(x: 25.56, y: -25.43)) + shapePath.line(to: NSPoint(x: 25.55, y: -15.31)) + shapePath.line(to: NSPoint(x: 25.55, y: -15.31)) + shapePath.close() + shapePath.move(to: NSPoint(x: 25.55, y: -46.27)) + shapePath.line(to: NSPoint(x: 30.61, y: -41.21)) + shapePath.line(to: NSPoint(x: 25.56, y: -36.17)) + shapePath.line(to: NSPoint(x: 25.55, y: -46.27)) + shapePath.line(to: NSPoint(x: 25.55, y: -46.27)) + shapePath.close() + shapePath.move(to: NSPoint(x: 20.16, y: -30.8)) + shapePath.line(to: NSPoint(x: 9.23, y: -19.83)) + shapePath.line(to: NSPoint(x: 12.39, y: -16.66)) + shapePath.line(to: NSPoint(x: 21.1, y: -25.37)) + shapePath.line(to: NSPoint(x: 21.1, y: -4.49)) + shapePath.line(to: NSPoint(x: 36.95, y: -20.33)) + shapePath.line(to: NSPoint(x: 26.48, y: -30.8)) + shapePath.line(to: NSPoint(x: 36.95, y: -41.24)) + shapePath.line(to: NSPoint(x: 21.1, y: -57.09)) + shapePath.line(to: NSPoint(x: 21.1, y: -36.21)) + shapePath.line(to: NSPoint(x: 12.39, y: -44.92)) + shapePath.line(to: NSPoint(x: 9.23, y: -41.75)) + shapePath.line(to: NSPoint(x: 20.16, y: -30.8)) + shapePath.line(to: NSPoint(x: 20.16, y: -30.8)) + shapePath.close() + shapePath.move(to: NSPoint(x: 23.09, y: -61.38)) + shapePath.curve(to: NSPoint(x: 45.65, y: -30.8), controlPoint1: NSPoint(x: 36.45, y: -61.38), controlPoint2: NSPoint(x: 45.65, y: -55.03)) + shapePath.curve(to: NSPoint(x: 23.09, y: -0.2), controlPoint1: NSPoint(x: 45.65, y: -6.54), controlPoint2: NSPoint(x: 36.45, y: -0.2)) + shapePath.curve(to: NSPoint(x: 0.52, y: -30.8), controlPoint1: NSPoint(x: 9.73, y: -0.2), controlPoint2: NSPoint(x: 0.52, y: -6.54)) + shapePath.curve(to: NSPoint(x: 23.09, y: -61.38), controlPoint1: NSPoint(x: 0.52, y: -55.03), controlPoint2: NSPoint(x: 9.73, y: -61.38)) + shapePath.line(to: NSPoint(x: 23.09, y: -61.38)) + shapePath.line(to: NSPoint(x: 23.09, y: -61.38)) + shapePath.close() + shapePath.windingRule = .evenOdd + StyleKit.bluetoothBlue.setFill() + shapePath.fill() + + + + + + + + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawBluetoothLogoDisabled(frame: NSRect = NSRect(x: 0, y: 0, width: 230, height: 230)) { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Group + NSGraphicsContext.saveGraphicsState() + context.translateBy(x: frame.minX + 0.13043 * frame.width, y: frame.minY + 0.99565 * frame.height) + context.scaleBy(x: 3.7, y: 3.7) + + + + //// Page-1 + //// Bluetooth_Smart_Logo + //// Shape Drawing + let shapePath = NSBezierPath() + shapePath.move(to: NSPoint(x: 25.55, y: -15.31)) + shapePath.line(to: NSPoint(x: 30.61, y: -20.37)) + shapePath.line(to: NSPoint(x: 25.56, y: -25.43)) + shapePath.line(to: NSPoint(x: 25.55, y: -15.31)) + shapePath.line(to: NSPoint(x: 25.55, y: -15.31)) + shapePath.close() + shapePath.move(to: NSPoint(x: 25.55, y: -46.27)) + shapePath.line(to: NSPoint(x: 30.61, y: -41.21)) + shapePath.line(to: NSPoint(x: 25.56, y: -36.17)) + shapePath.line(to: NSPoint(x: 25.55, y: -46.27)) + shapePath.line(to: NSPoint(x: 25.55, y: -46.27)) + shapePath.close() + shapePath.move(to: NSPoint(x: 20.16, y: -30.8)) + shapePath.line(to: NSPoint(x: 9.23, y: -19.83)) + shapePath.line(to: NSPoint(x: 12.39, y: -16.66)) + shapePath.line(to: NSPoint(x: 21.1, y: -25.37)) + shapePath.line(to: NSPoint(x: 21.1, y: -4.49)) + shapePath.line(to: NSPoint(x: 36.95, y: -20.33)) + shapePath.line(to: NSPoint(x: 26.48, y: -30.8)) + shapePath.line(to: NSPoint(x: 36.95, y: -41.24)) + shapePath.line(to: NSPoint(x: 21.1, y: -57.09)) + shapePath.line(to: NSPoint(x: 21.1, y: -36.21)) + shapePath.line(to: NSPoint(x: 12.39, y: -44.92)) + shapePath.line(to: NSPoint(x: 9.23, y: -41.75)) + shapePath.line(to: NSPoint(x: 20.16, y: -30.8)) + shapePath.line(to: NSPoint(x: 20.16, y: -30.8)) + shapePath.close() + shapePath.move(to: NSPoint(x: 23.09, y: -61.38)) + shapePath.curve(to: NSPoint(x: 45.65, y: -30.8), controlPoint1: NSPoint(x: 36.45, y: -61.38), controlPoint2: NSPoint(x: 45.65, y: -55.03)) + shapePath.curve(to: NSPoint(x: 23.09, y: -0.2), controlPoint1: NSPoint(x: 45.65, y: -6.54), controlPoint2: NSPoint(x: 36.45, y: -0.2)) + shapePath.curve(to: NSPoint(x: 0.52, y: -30.8), controlPoint1: NSPoint(x: 9.73, y: -0.2), controlPoint2: NSPoint(x: 0.52, y: -6.54)) + shapePath.curve(to: NSPoint(x: 23.09, y: -61.38), controlPoint1: NSPoint(x: 0.52, y: -55.03), controlPoint2: NSPoint(x: 9.73, y: -61.38)) + shapePath.line(to: NSPoint(x: 23.09, y: -61.38)) + shapePath.line(to: NSPoint(x: 23.09, y: -61.38)) + shapePath.close() + shapePath.windingRule = .evenOdd + StyleKit.bluetoothDisabledGrey.setFill() + shapePath.fill() + + + + + + + + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawScan1(frame: NSRect = NSRect(x: 0, y: 0, width: 230, height: 230)) { + + //// Bezier Drawing + let bezierPath = NSBezierPath() + bezierPath.move(to: NSPoint(x: frame.minX + 0.43125 * frame.width, y: frame.minY + 0.17500 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.10625 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.56875 * frame.width, y: frame.minY + 0.17500 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.20625 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.55000 * frame.width, y: frame.minY + 0.19375 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.52500 * frame.width, y: frame.minY + 0.20625 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.43125 * frame.width, y: frame.minY + 0.17500 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.47500 * frame.width, y: frame.minY + 0.20625 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.44375 * frame.width, y: frame.minY + 0.19375 * frame.height)) + bezierPath.close() + StyleKit.wirelessBlue.setFill() + bezierPath.fill() + } + + @objc dynamic public class func drawScan3(frame: NSRect = NSRect(x: 0, y: 0, width: 230, height: 230)) { + + //// Bezier Drawing + let bezierPath = NSBezierPath() + bezierPath.move(to: NSPoint(x: frame.minX + 0.43125 * frame.width, y: frame.minY + 0.17500 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.10625 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.56875 * frame.width, y: frame.minY + 0.17500 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.20625 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.55000 * frame.width, y: frame.minY + 0.19375 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.52500 * frame.width, y: frame.minY + 0.20625 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.43125 * frame.width, y: frame.minY + 0.17500 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.47500 * frame.width, y: frame.minY + 0.20625 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.44375 * frame.width, y: frame.minY + 0.19375 * frame.height)) + bezierPath.close() + StyleKit.wirelessBlue.setFill() + bezierPath.fill() + + + //// Bezier 3 Drawing + let bezier3Path = NSBezierPath() + bezier3Path.move(to: NSPoint(x: frame.minX + 0.14375 * frame.width, y: frame.minY + 0.46250 * frame.height)) + bezier3Path.line(to: NSPoint(x: frame.minX + 0.21250 * frame.width, y: frame.minY + 0.39375 * frame.height)) + bezier3Path.curve(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.50625 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.28750 * frame.width, y: frame.minY + 0.46250 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.38750 * frame.width, y: frame.minY + 0.50625 * frame.height)) + bezier3Path.curve(to: NSPoint(x: frame.minX + 0.78750 * frame.width, y: frame.minY + 0.38750 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.61250 * frame.width, y: frame.minY + 0.50625 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.71250 * frame.width, y: frame.minY + 0.46250 * frame.height)) + bezier3Path.line(to: NSPoint(x: frame.minX + 0.85625 * frame.width, y: frame.minY + 0.45625 * frame.height)) + bezier3Path.curve(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.61250 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.76875 * frame.width, y: frame.minY + 0.55625 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.63750 * frame.width, y: frame.minY + 0.61250 * frame.height)) + bezier3Path.curve(to: NSPoint(x: frame.minX + 0.14375 * frame.width, y: frame.minY + 0.46250 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.36250 * frame.width, y: frame.minY + 0.61250 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.23125 * frame.width, y: frame.minY + 0.55625 * frame.height)) + bezier3Path.close() + StyleKit.wirelessBlue.setFill() + bezier3Path.fill() + + + //// Bezier 4 Drawing + let bezier4Path = NSBezierPath() + bezier4Path.move(to: NSPoint(x: frame.minX + 0.28750 * frame.width, y: frame.minY + 0.31875 * frame.height)) + bezier4Path.line(to: NSPoint(x: frame.minX + 0.35625 * frame.width, y: frame.minY + 0.25000 * frame.height)) + bezier4Path.curve(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.30625 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.39375 * frame.width, y: frame.minY + 0.28750 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.44375 * frame.width, y: frame.minY + 0.30625 * frame.height)) + bezier4Path.curve(to: NSPoint(x: frame.minX + 0.64375 * frame.width, y: frame.minY + 0.25000 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.55625 * frame.width, y: frame.minY + 0.30625 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.60625 * frame.width, y: frame.minY + 0.28125 * frame.height)) + bezier4Path.line(to: NSPoint(x: frame.minX + 0.71250 * frame.width, y: frame.minY + 0.31875 * frame.height)) + bezier4Path.curve(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.40625 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.66250 * frame.width, y: frame.minY + 0.37500 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.58125 * frame.width, y: frame.minY + 0.40625 * frame.height)) + bezier4Path.curve(to: NSPoint(x: frame.minX + 0.28750 * frame.width, y: frame.minY + 0.31875 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.41875 * frame.width, y: frame.minY + 0.40625 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.33750 * frame.width, y: frame.minY + 0.37500 * frame.height)) + bezier4Path.close() + StyleKit.wirelessBlue.setFill() + bezier4Path.fill() + } + + @objc dynamic public class func drawScan4(frame: NSRect = NSRect(x: 0, y: 0, width: 230, height: 230)) { + + //// Bezier Drawing + let bezierPath = NSBezierPath() + bezierPath.move(to: NSPoint(x: frame.minX + 0.43125 * frame.width, y: frame.minY + 0.17500 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.10625 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.56875 * frame.width, y: frame.minY + 0.17500 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.20625 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.55000 * frame.width, y: frame.minY + 0.19375 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.52500 * frame.width, y: frame.minY + 0.20625 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.43125 * frame.width, y: frame.minY + 0.17500 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.47500 * frame.width, y: frame.minY + 0.20625 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.44375 * frame.width, y: frame.minY + 0.19375 * frame.height)) + bezierPath.close() + StyleKit.wirelessBlue.setFill() + bezierPath.fill() + + + //// Bezier 2 Drawing + let bezier2Path = NSBezierPath() + bezier2Path.move(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.71250 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.93125 * frame.width, y: frame.minY + 0.53750 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.66875 * frame.width, y: frame.minY + 0.71250 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.81875 * frame.width, y: frame.minY + 0.64375 * frame.height)) + bezier2Path.line(to: NSPoint(x: frame.minX + 1.00000 * frame.width, y: frame.minY + 0.60625 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.81250 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.87500 * frame.width, y: frame.minY + 0.73125 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.69375 * frame.width, y: frame.minY + 0.81250 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.00000 * frame.width, y: frame.minY + 0.60625 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.30625 * frame.width, y: frame.minY + 0.81250 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.12500 * frame.width, y: frame.minY + 0.73125 * frame.height)) + bezier2Path.line(to: NSPoint(x: frame.minX + 0.06875 * frame.width, y: frame.minY + 0.53750 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.71250 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.18125 * frame.width, y: frame.minY + 0.64375 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.33125 * frame.width, y: frame.minY + 0.71250 * frame.height)) + bezier2Path.close() + StyleKit.wirelessBlue.setFill() + bezier2Path.fill() + + + //// Bezier 3 Drawing + let bezier3Path = NSBezierPath() + bezier3Path.move(to: NSPoint(x: frame.minX + 0.14375 * frame.width, y: frame.minY + 0.46250 * frame.height)) + bezier3Path.line(to: NSPoint(x: frame.minX + 0.21250 * frame.width, y: frame.minY + 0.39375 * frame.height)) + bezier3Path.curve(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.50625 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.28750 * frame.width, y: frame.minY + 0.46250 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.38750 * frame.width, y: frame.minY + 0.50625 * frame.height)) + bezier3Path.curve(to: NSPoint(x: frame.minX + 0.78750 * frame.width, y: frame.minY + 0.38750 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.61250 * frame.width, y: frame.minY + 0.50625 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.71250 * frame.width, y: frame.minY + 0.46250 * frame.height)) + bezier3Path.line(to: NSPoint(x: frame.minX + 0.85625 * frame.width, y: frame.minY + 0.45625 * frame.height)) + bezier3Path.curve(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.61250 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.76875 * frame.width, y: frame.minY + 0.55625 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.63750 * frame.width, y: frame.minY + 0.61250 * frame.height)) + bezier3Path.curve(to: NSPoint(x: frame.minX + 0.14375 * frame.width, y: frame.minY + 0.46250 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.36250 * frame.width, y: frame.minY + 0.61250 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.23125 * frame.width, y: frame.minY + 0.55625 * frame.height)) + bezier3Path.close() + StyleKit.wirelessBlue.setFill() + bezier3Path.fill() + + + //// Bezier 4 Drawing + let bezier4Path = NSBezierPath() + bezier4Path.move(to: NSPoint(x: frame.minX + 0.28750 * frame.width, y: frame.minY + 0.31875 * frame.height)) + bezier4Path.line(to: NSPoint(x: frame.minX + 0.35625 * frame.width, y: frame.minY + 0.25000 * frame.height)) + bezier4Path.curve(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.30625 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.39375 * frame.width, y: frame.minY + 0.28750 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.44375 * frame.width, y: frame.minY + 0.30625 * frame.height)) + bezier4Path.curve(to: NSPoint(x: frame.minX + 0.64375 * frame.width, y: frame.minY + 0.25000 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.55625 * frame.width, y: frame.minY + 0.30625 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.60625 * frame.width, y: frame.minY + 0.28125 * frame.height)) + bezier4Path.line(to: NSPoint(x: frame.minX + 0.71250 * frame.width, y: frame.minY + 0.31875 * frame.height)) + bezier4Path.curve(to: NSPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.40625 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.66250 * frame.width, y: frame.minY + 0.37500 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.58125 * frame.width, y: frame.minY + 0.40625 * frame.height)) + bezier4Path.curve(to: NSPoint(x: frame.minX + 0.28750 * frame.width, y: frame.minY + 0.31875 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.41875 * frame.width, y: frame.minY + 0.40625 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.33750 * frame.width, y: frame.minY + 0.37500 * frame.height)) + bezier4Path.close() + StyleKit.wirelessBlue.setFill() + bezier4Path.fill() + } + + @objc dynamic public class func drawSetupLock(frame: NSRect = NSRect(x: 0, y: 0, width: 230, height: 230), setupLockGear: NSColor = NSColor(red: 0.425, green: 0.46, blue: 0.499, alpha: 1)) { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + // This non-generic function dramatically improves compilation times of complex expressions. + func fastFloor(_ x: CGFloat) -> CGFloat { return floor(x) } + + + //// Subframes + let group2: NSRect = NSRect(x: frame.minX + fastFloor(frame.width * 0.00000 + 0.5), y: frame.minY + fastFloor(frame.height * 0.01955 + 0) + 0.5, width: fastFloor(frame.width * 1.00000 - 0.5) - fastFloor(frame.width * 0.00000 + 0.5) + 1, height: fastFloor(frame.height * 0.99997 - 0.49) - fastFloor(frame.height * 0.01955 + 0) + 0.5) + + + //// Group 2 + NSGraphicsContext.saveGraphicsState() + context.beginTransparencyLayer(auxiliaryInfo: nil) + + //// Clip Clip + let clipPath = NSBezierPath() + clipPath.move(to: NSPoint(x: group2.minX + 0.55471 * group2.width, y: group2.minY + 0.00000 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.44959 * group2.width, y: group2.minY + 0.00000 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.37437 * group2.width, y: group2.minY + 0.13622 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.43378 * group2.width, y: group2.minY + 0.00000 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.42826 * group2.width, y: group2.minY + -0.00003 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.33274 * group2.width, y: group2.minY + 0.15349 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.20142 * group2.width, y: group2.minY + 0.10098 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.21934 * group2.width, y: group2.minY + 0.10098 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.20591 * group2.width, y: group2.minY + 0.10098 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.19274 * group2.width, y: group2.minY + 0.10098 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.18534 * group2.width, y: group2.minY + 0.10747 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.11083 * group2.width, y: group2.minY + 0.18187 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.15388 * group2.width, y: group2.minY + 0.33189 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.09950 * group2.width, y: group2.minY + 0.19341 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.09557 * group2.width, y: group2.minY + 0.19742 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.13691 * group2.width, y: group2.minY + 0.37287 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.00000 * group2.width, y: group2.minY + 0.44524 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.00000 * group2.width, y: group2.minY + 0.42294 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.00000 * group2.width, y: group2.minY + 0.42874 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.00000 * group2.width, y: group2.minY + 0.55041 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.13645 * group2.width, y: group2.minY + 0.62594 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.00000 * group2.width, y: group2.minY + 0.56689 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.00000 * group2.width, y: group2.minY + 0.57209 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.15343 * group2.width, y: group2.minY + 0.66700 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.10816 * group2.width, y: group2.minY + 0.81523 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.09175 * group2.width, y: group2.minY + 0.79943 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.09599 * group2.width, y: group2.minY + 0.80354 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.18829 * group2.width, y: group2.minY + 0.89540 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.19824 * group2.width, y: group2.minY + 0.89572 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.33176 * group2.width, y: group2.minY + 0.84604 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.21034 * group2.width, y: group2.minY + 0.89572 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.25522 * group2.width, y: group2.minY + 0.87901 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.37326 * group2.width, y: group2.minY + 0.86323 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.44512 * group2.width, y: group2.minY + 1.00000 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.42323 * group2.width, y: group2.minY + 1.00002 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.42936 * group2.width, y: group2.minY + 1.00000 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.55024 * group2.width, y: group2.minY + 1.00000 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.62541 * group2.width, y: group2.minY + 0.86369 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.56598 * group2.width, y: group2.minY + 1.00000 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.57152 * group2.width, y: group2.minY + 0.99998 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.66710 * group2.width, y: group2.minY + 0.84655 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.79846 * group2.width, y: group2.minY + 0.89898 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.78034 * group2.width, y: group2.minY + 0.89898 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.79394 * group2.width, y: group2.minY + 0.89898 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.80709 * group2.width, y: group2.minY + 0.89898 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.81449 * group2.width, y: group2.minY + 0.89261 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.88900 * group2.width, y: group2.minY + 0.81833 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.84590 * group2.width, y: group2.minY + 0.66814 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.90033 * group2.width, y: group2.minY + 0.80666 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.90421 * group2.width, y: group2.minY + 0.80267 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.86292 * group2.width, y: group2.minY + 0.62693 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 1.00000 * group2.width, y: group2.minY + 0.55476 * group2.height), controlPoint1: NSPoint(x: group2.minX + 1.00000 * group2.width, y: group2.minY + 0.57708 * group2.height), controlPoint2: NSPoint(x: group2.minX + 1.00000 * group2.width, y: group2.minY + 0.57129 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 1.00000 * group2.width, y: group2.minY + 0.44971 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.86337 * group2.width, y: group2.minY + 0.37408 * group2.height), controlPoint1: NSPoint(x: group2.minX + 1.00000 * group2.width, y: group2.minY + 0.43362 * group2.height), controlPoint2: NSPoint(x: group2.minX + 1.00000 * group2.width, y: group2.minY + 0.42796 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.84651 * group2.width, y: group2.minY + 0.33298 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.89233 * group2.width, y: group2.minY + 0.18518 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.90787 * group2.width, y: group2.minY + 0.20113 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.90382 * group2.width, y: group2.minY + 0.19700 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.81159 * group2.width, y: group2.minY + 0.10451 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.80158 * group2.width, y: group2.minY + 0.10423 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.66832 * group2.width, y: group2.minY + 0.15397 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.78954 * group2.width, y: group2.minY + 0.10423 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.74476 * group2.width, y: group2.minY + 0.12096 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.62658 * group2.width, y: group2.minY + 0.13676 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.55471 * group2.width, y: group2.minY + 0.00000 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.57648 * group2.width, y: group2.minY + 0.00000 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.57079 * group2.width, y: group2.minY + 0.00000 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.55471 * group2.width, y: group2.minY + 0.00000 * group2.height)) + clipPath.close() + clipPath.move(to: NSPoint(x: group2.minX + 0.46052 * group2.width, y: group2.minY + 0.04342 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.54343 * group2.width, y: group2.minY + 0.04342 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.58997 * group2.width, y: group2.minY + 0.16088 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.55294 * group2.width, y: group2.minY + 0.06321 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.57351 * group2.width, y: group2.minY + 0.11568 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.59329 * group2.width, y: group2.minY + 0.16988 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.66878 * group2.width, y: group2.minY + 0.20098 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.67690 * group2.width, y: group2.minY + 0.19747 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.79579 * group2.width, y: group2.minY + 0.14953 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.72189 * group2.width, y: group2.minY + 0.17789 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.77507 * group2.width, y: group2.minY + 0.15620 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.85405 * group2.width, y: group2.minY + 0.20768 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.80398 * group2.width, y: group2.minY + 0.32304 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.84665 * group2.width, y: group2.minY + 0.22861 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.82422 * group2.width, y: group2.minY + 0.27983 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.79990 * group2.width, y: group2.minY + 0.33182 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.83113 * group2.width, y: group2.minY + 0.40791 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.83964 * group2.width, y: group2.minY + 0.41126 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.95741 * group2.width, y: group2.minY + 0.46120 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.88508 * group2.width, y: group2.minY + 0.42911 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.93774 * group2.width, y: group2.minY + 0.45115 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.95741 * group2.width, y: group2.minY + 0.54289 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.83987 * group2.width, y: group2.minY + 0.58916 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.93751 * group2.width, y: group2.minY + 0.55244 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.88506 * group2.width, y: group2.minY + 0.57281 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.83102 * group2.width, y: group2.minY + 0.59239 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.79958 * group2.width, y: group2.minY + 0.66855 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.80333 * group2.width, y: group2.minY + 0.67714 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.85108 * group2.width, y: group2.minY + 0.79539 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.82267 * group2.width, y: group2.minY + 0.72168 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.84418 * group2.width, y: group2.minY + 0.77422 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.79266 * group2.width, y: group2.minY + 0.85366 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.67648 * group2.width, y: group2.minY + 0.80312 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.77381 * group2.width, y: group2.minY + 0.84695 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.72101 * group2.width, y: group2.minY + 0.82386 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.66818 * group2.width, y: group2.minY + 0.79924 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.59269 * group2.width, y: group2.minY + 0.83029 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.58932 * group2.width, y: group2.minY + 0.83886 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.53930 * group2.width, y: group2.minY + 0.95652 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.57148 * group2.width, y: group2.minY + 0.88431 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.54930 * group2.width, y: group2.minY + 0.93701 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.45645 * group2.width, y: group2.minY + 0.95652 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.40996 * group2.width, y: group2.minY + 0.83911 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.44689 * group2.width, y: group2.minY + 0.93678 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.42639 * group2.width, y: group2.minY + 0.88429 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.40670 * group2.width, y: group2.minY + 0.83016 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.33142 * group2.width, y: group2.minY + 0.79895 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.32330 * group2.width, y: group2.minY + 0.80249 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.20413 * group2.width, y: group2.minY + 0.85036 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.27821 * group2.width, y: group2.minY + 0.82204 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.22492 * group2.width, y: group2.minY + 0.84370 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.14597 * group2.width, y: group2.minY + 0.79218 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.19594 * group2.width, y: group2.minY + 0.67689 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.15327 * group2.width, y: group2.minY + 0.77128 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.17577 * group2.width, y: group2.minY + 0.72009 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.20008 * group2.width, y: group2.minY + 0.66809 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.16868 * group2.width, y: group2.minY + 0.59203 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.16024 * group2.width, y: group2.minY + 0.58870 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.04262 * group2.width, y: group2.minY + 0.53883 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.11486 * group2.width, y: group2.minY + 0.57087 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.06230 * group2.width, y: group2.minY + 0.54884 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.04262 * group2.width, y: group2.minY + 0.45704 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.16008 * group2.width, y: group2.minY + 0.41061 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.06252 * group2.width, y: group2.minY + 0.44745 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.11491 * group2.width, y: group2.minY + 0.42703 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.16886 * group2.width, y: group2.minY + 0.40735 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.20031 * group2.width, y: group2.minY + 0.33144 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.19655 * group2.width, y: group2.minY + 0.32287 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.14881 * group2.width, y: group2.minY + 0.20477 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.17721 * group2.width, y: group2.minY + 0.27836 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.15572 * group2.width, y: group2.minY + 0.22589 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.20734 * group2.width, y: group2.minY + 0.14631 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.32346 * group2.width, y: group2.minY + 0.19693 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.22615 * group2.width, y: group2.minY + 0.15308 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.27887 * group2.width, y: group2.minY + 0.17617 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.33181 * group2.width, y: group2.minY + 0.20083 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.40714 * group2.width, y: group2.minY + 0.16959 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.41051 * group2.width, y: group2.minY + 0.16102 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.46052 * group2.width, y: group2.minY + 0.04342 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.42841 * group2.width, y: group2.minY + 0.11555 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.45052 * group2.width, y: group2.minY + 0.06294 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.46052 * group2.width, y: group2.minY + 0.04342 * group2.height)) + clipPath.close() + clipPath.move(to: NSPoint(x: group2.minX + 0.50000 * group2.width, y: group2.minY + 0.32498 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.32512 * group2.width, y: group2.minY + 0.50001 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.40361 * group2.width, y: group2.minY + 0.32498 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.32512 * group2.width, y: group2.minY + 0.40350 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.50000 * group2.width, y: group2.minY + 0.67492 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.32512 * group2.width, y: group2.minY + 0.59645 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.40359 * group2.width, y: group2.minY + 0.67492 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.67463 * group2.width, y: group2.minY + 0.50001 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.59633 * group2.width, y: group2.minY + 0.67492 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.67463 * group2.width, y: group2.minY + 0.59643 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.50000 * group2.width, y: group2.minY + 0.32498 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.67463 * group2.width, y: group2.minY + 0.40350 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.59633 * group2.width, y: group2.minY + 0.32498 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.50000 * group2.width, y: group2.minY + 0.32498 * group2.height)) + clipPath.close() + clipPath.move(to: NSPoint(x: group2.minX + 0.50000 * group2.width, y: group2.minY + 0.63146 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.36773 * group2.width, y: group2.minY + 0.50001 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.42704 * group2.width, y: group2.minY + 0.63146 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.36773 * group2.width, y: group2.minY + 0.57248 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.50000 * group2.width, y: group2.minY + 0.36845 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.36773 * group2.width, y: group2.minY + 0.42748 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.42702 * group2.width, y: group2.minY + 0.36845 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.63209 * group2.width, y: group2.minY + 0.50001 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.57283 * group2.width, y: group2.minY + 0.36845 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.63209 * group2.width, y: group2.minY + 0.42748 * group2.height)) + clipPath.curve(to: NSPoint(x: group2.minX + 0.50000 * group2.width, y: group2.minY + 0.63146 * group2.height), controlPoint1: NSPoint(x: group2.minX + 0.63207 * group2.width, y: group2.minY + 0.57248 * group2.height), controlPoint2: NSPoint(x: group2.minX + 0.57283 * group2.width, y: group2.minY + 0.63146 * group2.height)) + clipPath.line(to: NSPoint(x: group2.minX + 0.50000 * group2.width, y: group2.minY + 0.63146 * group2.height)) + clipPath.close() + clipPath.windingRule = .evenOdd + clipPath.addClip() + + + //// Rectangle Drawing + let rectanglePath = NSBezierPath(rect: NSRect(x: group2.minX + fastFloor(group2.width * -0.20000 + 0.5), y: group2.minY + fastFloor(group2.height * -0.20397 + 0.5) + 0, width: fastFloor(group2.width * 1.20001 + 0.5) - fastFloor(group2.width * -0.20000 + 0.5), height: fastFloor(group2.height * 1.20403 - 0) - fastFloor(group2.height * -0.20397 + 0.5) + 0.5)) + setupLockGear.setFill() + rectanglePath.fill() + + + context.endTransparencyLayer() + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawSetupLockSelected(frame: NSRect = NSRect(x: 0, y: 0, width: 230, height: 230), setupLockGear: NSColor = NSColor(red: 0.425, green: 0.46, blue: 0.499, alpha: 1)) { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + // This non-generic function dramatically improves compilation times of complex expressions. + func fastFloor(_ x: CGFloat) -> CGFloat { return floor(x) } + + + //// Subframes + let group: NSRect = NSRect(x: frame.minX + fastFloor(frame.width * 0.00000 + 0.5), y: frame.minY + fastFloor(frame.height * 0.01955 + 0) + 0.5, width: fastFloor(frame.width * 1.00000 - 0.5) - fastFloor(frame.width * 0.00000 + 0.5) + 1, height: fastFloor(frame.height * 0.99997 - 0.49) - fastFloor(frame.height * 0.01955 + 0) + 0.5) + + + //// Group + //// Group 2 + NSGraphicsContext.saveGraphicsState() + context.beginTransparencyLayer(auxiliaryInfo: nil) + + //// Clip Clip + let clipPath = NSBezierPath() + clipPath.move(to: NSPoint(x: group.minX + 0.55471 * group.width, y: group.minY + 0.00000 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.44959 * group.width, y: group.minY + 0.00000 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.37437 * group.width, y: group.minY + 0.13622 * group.height), controlPoint1: NSPoint(x: group.minX + 0.43378 * group.width, y: group.minY + 0.00000 * group.height), controlPoint2: NSPoint(x: group.minX + 0.42826 * group.width, y: group.minY + -0.00003 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.33274 * group.width, y: group.minY + 0.15349 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.20142 * group.width, y: group.minY + 0.10098 * group.height), controlPoint1: NSPoint(x: group.minX + 0.21934 * group.width, y: group.minY + 0.10098 * group.height), controlPoint2: NSPoint(x: group.minX + 0.20591 * group.width, y: group.minY + 0.10098 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.19274 * group.width, y: group.minY + 0.10098 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.18534 * group.width, y: group.minY + 0.10747 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.11083 * group.width, y: group.minY + 0.18187 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.15388 * group.width, y: group.minY + 0.33189 * group.height), controlPoint1: NSPoint(x: group.minX + 0.09950 * group.width, y: group.minY + 0.19341 * group.height), controlPoint2: NSPoint(x: group.minX + 0.09557 * group.width, y: group.minY + 0.19742 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.13691 * group.width, y: group.minY + 0.37287 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.00000 * group.width, y: group.minY + 0.44524 * group.height), controlPoint1: NSPoint(x: group.minX + 0.00000 * group.width, y: group.minY + 0.42294 * group.height), controlPoint2: NSPoint(x: group.minX + 0.00000 * group.width, y: group.minY + 0.42874 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.00000 * group.width, y: group.minY + 0.55041 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.13645 * group.width, y: group.minY + 0.62594 * group.height), controlPoint1: NSPoint(x: group.minX + 0.00000 * group.width, y: group.minY + 0.56689 * group.height), controlPoint2: NSPoint(x: group.minX + 0.00000 * group.width, y: group.minY + 0.57209 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.15343 * group.width, y: group.minY + 0.66700 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.10816 * group.width, y: group.minY + 0.81523 * group.height), controlPoint1: NSPoint(x: group.minX + 0.09175 * group.width, y: group.minY + 0.79943 * group.height), controlPoint2: NSPoint(x: group.minX + 0.09599 * group.width, y: group.minY + 0.80354 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.18829 * group.width, y: group.minY + 0.89540 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.19824 * group.width, y: group.minY + 0.89572 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.33176 * group.width, y: group.minY + 0.84604 * group.height), controlPoint1: NSPoint(x: group.minX + 0.21034 * group.width, y: group.minY + 0.89572 * group.height), controlPoint2: NSPoint(x: group.minX + 0.25522 * group.width, y: group.minY + 0.87901 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.37326 * group.width, y: group.minY + 0.86323 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.44512 * group.width, y: group.minY + 1.00000 * group.height), controlPoint1: NSPoint(x: group.minX + 0.42323 * group.width, y: group.minY + 1.00002 * group.height), controlPoint2: NSPoint(x: group.minX + 0.42936 * group.width, y: group.minY + 1.00000 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.55024 * group.width, y: group.minY + 1.00000 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.62541 * group.width, y: group.minY + 0.86369 * group.height), controlPoint1: NSPoint(x: group.minX + 0.56598 * group.width, y: group.minY + 1.00000 * group.height), controlPoint2: NSPoint(x: group.minX + 0.57152 * group.width, y: group.minY + 0.99998 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.66710 * group.width, y: group.minY + 0.84655 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.79846 * group.width, y: group.minY + 0.89898 * group.height), controlPoint1: NSPoint(x: group.minX + 0.78034 * group.width, y: group.minY + 0.89898 * group.height), controlPoint2: NSPoint(x: group.minX + 0.79394 * group.width, y: group.minY + 0.89898 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.80709 * group.width, y: group.minY + 0.89898 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.81449 * group.width, y: group.minY + 0.89261 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.88900 * group.width, y: group.minY + 0.81833 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.84590 * group.width, y: group.minY + 0.66814 * group.height), controlPoint1: NSPoint(x: group.minX + 0.90033 * group.width, y: group.minY + 0.80666 * group.height), controlPoint2: NSPoint(x: group.minX + 0.90421 * group.width, y: group.minY + 0.80267 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.86292 * group.width, y: group.minY + 0.62693 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 1.00000 * group.width, y: group.minY + 0.55476 * group.height), controlPoint1: NSPoint(x: group.minX + 1.00000 * group.width, y: group.minY + 0.57708 * group.height), controlPoint2: NSPoint(x: group.minX + 1.00000 * group.width, y: group.minY + 0.57129 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 1.00000 * group.width, y: group.minY + 0.44971 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.86337 * group.width, y: group.minY + 0.37408 * group.height), controlPoint1: NSPoint(x: group.minX + 1.00000 * group.width, y: group.minY + 0.43362 * group.height), controlPoint2: NSPoint(x: group.minX + 1.00000 * group.width, y: group.minY + 0.42796 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.84651 * group.width, y: group.minY + 0.33298 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.89233 * group.width, y: group.minY + 0.18518 * group.height), controlPoint1: NSPoint(x: group.minX + 0.90787 * group.width, y: group.minY + 0.20113 * group.height), controlPoint2: NSPoint(x: group.minX + 0.90382 * group.width, y: group.minY + 0.19700 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.81159 * group.width, y: group.minY + 0.10451 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.80158 * group.width, y: group.minY + 0.10423 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.66832 * group.width, y: group.minY + 0.15397 * group.height), controlPoint1: NSPoint(x: group.minX + 0.78954 * group.width, y: group.minY + 0.10423 * group.height), controlPoint2: NSPoint(x: group.minX + 0.74476 * group.width, y: group.minY + 0.12096 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.62658 * group.width, y: group.minY + 0.13676 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.55471 * group.width, y: group.minY + 0.00000 * group.height), controlPoint1: NSPoint(x: group.minX + 0.57648 * group.width, y: group.minY + 0.00000 * group.height), controlPoint2: NSPoint(x: group.minX + 0.57079 * group.width, y: group.minY + 0.00000 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.55471 * group.width, y: group.minY + 0.00000 * group.height)) + clipPath.close() + clipPath.move(to: NSPoint(x: group.minX + 0.46052 * group.width, y: group.minY + 0.04342 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.54343 * group.width, y: group.minY + 0.04342 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.58997 * group.width, y: group.minY + 0.16088 * group.height), controlPoint1: NSPoint(x: group.minX + 0.55294 * group.width, y: group.minY + 0.06321 * group.height), controlPoint2: NSPoint(x: group.minX + 0.57351 * group.width, y: group.minY + 0.11568 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.59329 * group.width, y: group.minY + 0.16988 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.66878 * group.width, y: group.minY + 0.20098 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.67690 * group.width, y: group.minY + 0.19747 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.79579 * group.width, y: group.minY + 0.14953 * group.height), controlPoint1: NSPoint(x: group.minX + 0.72189 * group.width, y: group.minY + 0.17789 * group.height), controlPoint2: NSPoint(x: group.minX + 0.77507 * group.width, y: group.minY + 0.15620 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.85405 * group.width, y: group.minY + 0.20768 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.80398 * group.width, y: group.minY + 0.32304 * group.height), controlPoint1: NSPoint(x: group.minX + 0.84665 * group.width, y: group.minY + 0.22861 * group.height), controlPoint2: NSPoint(x: group.minX + 0.82422 * group.width, y: group.minY + 0.27983 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.79990 * group.width, y: group.minY + 0.33182 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.83113 * group.width, y: group.minY + 0.40791 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.83964 * group.width, y: group.minY + 0.41126 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.95741 * group.width, y: group.minY + 0.46120 * group.height), controlPoint1: NSPoint(x: group.minX + 0.88508 * group.width, y: group.minY + 0.42911 * group.height), controlPoint2: NSPoint(x: group.minX + 0.93774 * group.width, y: group.minY + 0.45115 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.95741 * group.width, y: group.minY + 0.54289 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.83987 * group.width, y: group.minY + 0.58916 * group.height), controlPoint1: NSPoint(x: group.minX + 0.93751 * group.width, y: group.minY + 0.55244 * group.height), controlPoint2: NSPoint(x: group.minX + 0.88506 * group.width, y: group.minY + 0.57281 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.83102 * group.width, y: group.minY + 0.59239 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.79958 * group.width, y: group.minY + 0.66855 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.80333 * group.width, y: group.minY + 0.67714 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.85108 * group.width, y: group.minY + 0.79539 * group.height), controlPoint1: NSPoint(x: group.minX + 0.82267 * group.width, y: group.minY + 0.72168 * group.height), controlPoint2: NSPoint(x: group.minX + 0.84418 * group.width, y: group.minY + 0.77422 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.79266 * group.width, y: group.minY + 0.85366 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.67648 * group.width, y: group.minY + 0.80312 * group.height), controlPoint1: NSPoint(x: group.minX + 0.77381 * group.width, y: group.minY + 0.84695 * group.height), controlPoint2: NSPoint(x: group.minX + 0.72101 * group.width, y: group.minY + 0.82386 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.66818 * group.width, y: group.minY + 0.79924 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.59269 * group.width, y: group.minY + 0.83029 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.58932 * group.width, y: group.minY + 0.83886 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.53930 * group.width, y: group.minY + 0.95652 * group.height), controlPoint1: NSPoint(x: group.minX + 0.57148 * group.width, y: group.minY + 0.88431 * group.height), controlPoint2: NSPoint(x: group.minX + 0.54930 * group.width, y: group.minY + 0.93701 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.45645 * group.width, y: group.minY + 0.95652 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.40996 * group.width, y: group.minY + 0.83911 * group.height), controlPoint1: NSPoint(x: group.minX + 0.44689 * group.width, y: group.minY + 0.93678 * group.height), controlPoint2: NSPoint(x: group.minX + 0.42639 * group.width, y: group.minY + 0.88429 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.40670 * group.width, y: group.minY + 0.83016 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.33142 * group.width, y: group.minY + 0.79895 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.32330 * group.width, y: group.minY + 0.80249 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.20413 * group.width, y: group.minY + 0.85036 * group.height), controlPoint1: NSPoint(x: group.minX + 0.27821 * group.width, y: group.minY + 0.82204 * group.height), controlPoint2: NSPoint(x: group.minX + 0.22492 * group.width, y: group.minY + 0.84370 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.14597 * group.width, y: group.minY + 0.79218 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.19594 * group.width, y: group.minY + 0.67689 * group.height), controlPoint1: NSPoint(x: group.minX + 0.15327 * group.width, y: group.minY + 0.77128 * group.height), controlPoint2: NSPoint(x: group.minX + 0.17577 * group.width, y: group.minY + 0.72009 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.20008 * group.width, y: group.minY + 0.66809 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.16868 * group.width, y: group.minY + 0.59203 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.16024 * group.width, y: group.minY + 0.58870 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.04262 * group.width, y: group.minY + 0.53883 * group.height), controlPoint1: NSPoint(x: group.minX + 0.11486 * group.width, y: group.minY + 0.57087 * group.height), controlPoint2: NSPoint(x: group.minX + 0.06230 * group.width, y: group.minY + 0.54884 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.04262 * group.width, y: group.minY + 0.45704 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.16008 * group.width, y: group.minY + 0.41061 * group.height), controlPoint1: NSPoint(x: group.minX + 0.06252 * group.width, y: group.minY + 0.44745 * group.height), controlPoint2: NSPoint(x: group.minX + 0.11491 * group.width, y: group.minY + 0.42703 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.16886 * group.width, y: group.minY + 0.40735 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.20031 * group.width, y: group.minY + 0.33144 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.19655 * group.width, y: group.minY + 0.32287 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.14881 * group.width, y: group.minY + 0.20477 * group.height), controlPoint1: NSPoint(x: group.minX + 0.17721 * group.width, y: group.minY + 0.27836 * group.height), controlPoint2: NSPoint(x: group.minX + 0.15572 * group.width, y: group.minY + 0.22589 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.20734 * group.width, y: group.minY + 0.14631 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.32346 * group.width, y: group.minY + 0.19693 * group.height), controlPoint1: NSPoint(x: group.minX + 0.22615 * group.width, y: group.minY + 0.15308 * group.height), controlPoint2: NSPoint(x: group.minX + 0.27887 * group.width, y: group.minY + 0.17617 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.33181 * group.width, y: group.minY + 0.20083 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.40714 * group.width, y: group.minY + 0.16959 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.41051 * group.width, y: group.minY + 0.16102 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.46052 * group.width, y: group.minY + 0.04342 * group.height), controlPoint1: NSPoint(x: group.minX + 0.42841 * group.width, y: group.minY + 0.11555 * group.height), controlPoint2: NSPoint(x: group.minX + 0.45052 * group.width, y: group.minY + 0.06294 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.46052 * group.width, y: group.minY + 0.04342 * group.height)) + clipPath.close() + clipPath.move(to: NSPoint(x: group.minX + 0.50000 * group.width, y: group.minY + 0.32498 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.32512 * group.width, y: group.minY + 0.50001 * group.height), controlPoint1: NSPoint(x: group.minX + 0.40361 * group.width, y: group.minY + 0.32498 * group.height), controlPoint2: NSPoint(x: group.minX + 0.32512 * group.width, y: group.minY + 0.40350 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.50000 * group.width, y: group.minY + 0.67492 * group.height), controlPoint1: NSPoint(x: group.minX + 0.32512 * group.width, y: group.minY + 0.59645 * group.height), controlPoint2: NSPoint(x: group.minX + 0.40359 * group.width, y: group.minY + 0.67492 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.67463 * group.width, y: group.minY + 0.50001 * group.height), controlPoint1: NSPoint(x: group.minX + 0.59633 * group.width, y: group.minY + 0.67492 * group.height), controlPoint2: NSPoint(x: group.minX + 0.67463 * group.width, y: group.minY + 0.59643 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.50000 * group.width, y: group.minY + 0.32498 * group.height), controlPoint1: NSPoint(x: group.minX + 0.67463 * group.width, y: group.minY + 0.40350 * group.height), controlPoint2: NSPoint(x: group.minX + 0.59633 * group.width, y: group.minY + 0.32498 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.50000 * group.width, y: group.minY + 0.32498 * group.height)) + clipPath.close() + clipPath.move(to: NSPoint(x: group.minX + 0.50000 * group.width, y: group.minY + 0.63146 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.36773 * group.width, y: group.minY + 0.50001 * group.height), controlPoint1: NSPoint(x: group.minX + 0.42704 * group.width, y: group.minY + 0.63146 * group.height), controlPoint2: NSPoint(x: group.minX + 0.36773 * group.width, y: group.minY + 0.57248 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.50000 * group.width, y: group.minY + 0.36845 * group.height), controlPoint1: NSPoint(x: group.minX + 0.36773 * group.width, y: group.minY + 0.42748 * group.height), controlPoint2: NSPoint(x: group.minX + 0.42702 * group.width, y: group.minY + 0.36845 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.63209 * group.width, y: group.minY + 0.50001 * group.height), controlPoint1: NSPoint(x: group.minX + 0.57283 * group.width, y: group.minY + 0.36845 * group.height), controlPoint2: NSPoint(x: group.minX + 0.63209 * group.width, y: group.minY + 0.42748 * group.height)) + clipPath.curve(to: NSPoint(x: group.minX + 0.50000 * group.width, y: group.minY + 0.63146 * group.height), controlPoint1: NSPoint(x: group.minX + 0.63207 * group.width, y: group.minY + 0.57248 * group.height), controlPoint2: NSPoint(x: group.minX + 0.57283 * group.width, y: group.minY + 0.63146 * group.height)) + clipPath.line(to: NSPoint(x: group.minX + 0.50000 * group.width, y: group.minY + 0.63146 * group.height)) + clipPath.close() + clipPath.windingRule = .evenOdd + clipPath.addClip() + + + //// Rectangle Drawing + let rectanglePath = NSBezierPath(rect: NSRect(x: group.minX + fastFloor(group.width * -0.20000 + 0.5), y: group.minY + fastFloor(group.height * -0.20397 + 0.5) + 0, width: fastFloor(group.width * 1.20001 + 0.5) - fastFloor(group.width * -0.20000 + 0.5), height: fastFloor(group.height * 1.20403 - 0) - fastFloor(group.height * -0.20397 + 0.5) + 0.5)) + setupLockGear.setFill() + rectanglePath.fill() + + + context.endTransparencyLayer() + NSGraphicsContext.restoreGraphicsState() + + + //// Bezier Drawing + let bezierPath = NSBezierPath() + bezierPath.move(to: NSPoint(x: group.minX + 0.54751 * group.width, y: group.minY + 0.97747 * group.height)) + bezierPath.curve(to: NSPoint(x: group.minX + 0.61250 * group.width, y: group.minY + 0.85216 * group.height), controlPoint1: NSPoint(x: group.minX + 0.54750 * group.width, y: group.minY + 0.97748 * group.height), controlPoint2: NSPoint(x: group.minX + 0.61250 * group.width, y: group.minY + 0.85216 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.66750 * group.width, y: group.minY + 0.82710 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.79250 * group.width, y: group.minY + 0.88224 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.87750 * group.width, y: group.minY + 0.79703 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.82750 * group.width, y: group.minY + 0.67672 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.85250 * group.width, y: group.minY + 0.61156 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.97750 * group.width, y: group.minY + 0.55141 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.97750 * group.width, y: group.minY + 0.45617 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.85250 * group.width, y: group.minY + 0.39100 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.82750 * group.width, y: group.minY + 0.34088 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.87750 * group.width, y: group.minY + 0.21055 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.80750 * group.width, y: group.minY + 0.12534 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.67750 * group.width, y: group.minY + 0.17546 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.61250 * group.width, y: group.minY + 0.15541 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.54750 * group.width, y: group.minY + 0.02508 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.45250 * group.width, y: group.minY + 0.02508 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.38750 * group.width, y: group.minY + 0.15541 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.33250 * group.width, y: group.minY + 0.17546 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.20750 * group.width, y: group.minY + 0.12534 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.13250 * group.width, y: group.minY + 0.19551 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.17250 * group.width, y: group.minY + 0.32083 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.15250 * group.width, y: group.minY + 0.39100 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.02250 * group.width, y: group.minY + 0.44113 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.02250 * group.width, y: group.minY + 0.55141 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.15250 * group.width, y: group.minY + 0.61156 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.17250 * group.width, y: group.minY + 0.67672 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.12250 * group.width, y: group.minY + 0.79703 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.19750 * group.width, y: group.minY + 0.87221 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.33250 * group.width, y: group.minY + 0.82710 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.38750 * group.width, y: group.minY + 0.85216 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.44250 * group.width, y: group.minY + 0.97748 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.54750 * group.width, y: group.minY + 0.97748 * group.height)) + bezierPath.line(to: NSPoint(x: group.minX + 0.54751 * group.width, y: group.minY + 0.97747 * group.height)) + bezierPath.close() + bezierPath.move(to: NSPoint(x: group.minX + 0.40854 * group.width, y: group.minY + 0.59524 * group.height)) + bezierPath.curve(to: NSPoint(x: group.minX + 0.40631 * group.width, y: group.minY + 0.59307 * group.height), controlPoint1: NSPoint(x: group.minX + 0.40779 * group.width, y: group.minY + 0.59453 * group.height), controlPoint2: NSPoint(x: group.minX + 0.40705 * group.width, y: group.minY + 0.59380 * group.height)) + bezierPath.curve(to: NSPoint(x: group.minX + 0.40631 * group.width, y: group.minY + 0.40699 * group.height), controlPoint1: NSPoint(x: group.minX + 0.35457 * group.width, y: group.minY + 0.54168 * group.height), controlPoint2: NSPoint(x: group.minX + 0.35457 * group.width, y: group.minY + 0.45837 * group.height)) + bezierPath.curve(to: NSPoint(x: group.minX + 0.59369 * group.width, y: group.minY + 0.40699 * group.height), controlPoint1: NSPoint(x: group.minX + 0.45806 * group.width, y: group.minY + 0.35560 * group.height), controlPoint2: NSPoint(x: group.minX + 0.54195 * group.width, y: group.minY + 0.35560 * group.height)) + bezierPath.curve(to: NSPoint(x: group.minX + 0.59369 * group.width, y: group.minY + 0.59307 * group.height), controlPoint1: NSPoint(x: group.minX + 0.64544 * group.width, y: group.minY + 0.45837 * group.height), controlPoint2: NSPoint(x: group.minX + 0.64544 * group.width, y: group.minY + 0.54168 * group.height)) + bezierPath.curve(to: NSPoint(x: group.minX + 0.40854 * group.width, y: group.minY + 0.59524 * group.height), controlPoint1: NSPoint(x: group.minX + 0.54269 * group.width, y: group.minY + 0.64372 * group.height), controlPoint2: NSPoint(x: group.minX + 0.46044 * group.width, y: group.minY + 0.64445 * group.height)) + bezierPath.close() + setupLockGear.setFill() + bezierPath.fill() + setupLockGear.setStroke() + bezierPath.lineWidth = 1 + bezierPath.stroke() + } + + @objc dynamic public class func drawSetupKey(frame: NSRect = NSRect(x: 0, y: 0, width: 230, height: 230)) { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Color Declarations + let setupKeyFill = NSColor(red: 0, green: 0, blue: 0, alpha: 1) + + //// Group + NSGraphicsContext.saveGraphicsState() + context.translateBy(x: frame.minX + 0.00000 * frame.width, y: frame.minY + 1.00000 * frame.height) + context.scaleBy(x: 9.2, y: 9.2) + + + + //// Bezier Drawing + let bezierPath = NSBezierPath() + bezierPath.move(to: NSPoint(x: 9.53, y: -11.6)) + bezierPath.curve(to: NSPoint(x: 8.97, y: -12.21), controlPoint1: NSPoint(x: 9.32, y: -11.77), controlPoint2: NSPoint(x: 9.13, y: -11.97)) + bezierPath.curve(to: NSPoint(x: 7.63, y: -14.79), controlPoint1: NSPoint(x: 8.3, y: -13.49), controlPoint2: NSPoint(x: 8.3, y: -13.49)) + bezierPath.curve(to: NSPoint(x: 3.56, y: -22.58), controlPoint1: NSPoint(x: 5.59, y: -18.69), controlPoint2: NSPoint(x: 4.76, y: -20.29)) + bezierPath.line(to: NSPoint(x: 3.41, y: -22.67)) + bezierPath.curve(to: NSPoint(x: 1.92, y: -22.7), controlPoint1: NSPoint(x: 2.59, y: -22.6), controlPoint2: NSPoint(x: 2.27, y: -22.6)) + bezierPath.line(to: NSPoint(x: 1.75, y: -22.64)) + bezierPath.curve(to: NSPoint(x: 1.02, y: -20.01), controlPoint1: NSPoint(x: 1.22, y: -21.79), controlPoint2: NSPoint(x: 1.02, y: -20.99)) + bezierPath.line(to: NSPoint(x: 1.13, y: -19.86)) + bezierPath.curve(to: NSPoint(x: 1.39, y: -19.77), controlPoint1: NSPoint(x: 1.26, y: -19.82), controlPoint2: NSPoint(x: 1.26, y: -19.82)) + bezierPath.curve(to: NSPoint(x: 1.66, y: -19.69), controlPoint1: NSPoint(x: 1.52, y: -19.73), controlPoint2: NSPoint(x: 1.52, y: -19.73)) + bezierPath.curve(to: NSPoint(x: 1.79, y: -19.65), controlPoint1: NSPoint(x: 1.72, y: -19.67), controlPoint2: NSPoint(x: 1.72, y: -19.67)) + bezierPath.line(to: NSPoint(x: 1.72, y: -19.69)) + bezierPath.curve(to: NSPoint(x: 2.12, y: -19.05), controlPoint1: NSPoint(x: 1.9, y: -19.51), controlPoint2: NSPoint(x: 2.03, y: -19.3)) + bezierPath.line(to: NSPoint(x: 2.11, y: -19.12)) + bezierPath.curve(to: NSPoint(x: 2.07, y: -18.8), controlPoint1: NSPoint(x: 2.09, y: -18.96), controlPoint2: NSPoint(x: 2.09, y: -18.96)) + bezierPath.curve(to: NSPoint(x: 2.03, y: -18.48), controlPoint1: NSPoint(x: 2.05, y: -18.64), controlPoint2: NSPoint(x: 2.05, y: -18.64)) + bezierPath.line(to: NSPoint(x: 2.12, y: -18.32)) + bezierPath.curve(to: NSPoint(x: 3, y: -17.93), controlPoint1: NSPoint(x: 2.56, y: -18.13), controlPoint2: NSPoint(x: 2.56, y: -18.13)) + bezierPath.curve(to: NSPoint(x: 3.28, y: -17.45), controlPoint1: NSPoint(x: 3.09, y: -17.87), controlPoint2: NSPoint(x: 3.19, y: -17.7)) + bezierPath.line(to: NSPoint(x: 3.27, y: -17.5)) + bezierPath.curve(to: NSPoint(x: 3.27, y: -17.36), controlPoint1: NSPoint(x: 3.27, y: -17.43), controlPoint2: NSPoint(x: 3.27, y: -17.43)) + bezierPath.curve(to: NSPoint(x: 3.27, y: -16.8), controlPoint1: NSPoint(x: 3.27, y: -17.08), controlPoint2: NSPoint(x: 3.27, y: -17.08)) + bezierPath.line(to: NSPoint(x: 3.37, y: -16.66)) + bezierPath.curve(to: NSPoint(x: 4.19, y: -16.35), controlPoint1: NSPoint(x: 3.78, y: -16.51), controlPoint2: NSPoint(x: 3.78, y: -16.51)) + bezierPath.line(to: NSPoint(x: 4.13, y: -16.39)) + bezierPath.curve(to: NSPoint(x: 4.47, y: -15.77), controlPoint1: NSPoint(x: 4.29, y: -16.23), controlPoint2: NSPoint(x: 4.4, y: -16.02)) + bezierPath.curve(to: NSPoint(x: 4.34, y: -15.39), controlPoint1: NSPoint(x: 4.46, y: -15.72), controlPoint2: NSPoint(x: 4.43, y: -15.62)) + bezierPath.line(to: NSPoint(x: 4.34, y: -15.39)) + bezierPath.curve(to: NSPoint(x: 4.52, y: -14.36), controlPoint1: NSPoint(x: 4.11, y: -14.81), controlPoint2: NSPoint(x: 4.1, y: -14.54)) + bezierPath.line(to: NSPoint(x: 4.45, y: -14.43)) + bezierPath.curve(to: NSPoint(x: 4.88, y: -13.71), controlPoint1: NSPoint(x: 4.66, y: -14.07), controlPoint2: NSPoint(x: 4.66, y: -14.07)) + bezierPath.line(to: NSPoint(x: 4.86, y: -13.83)) + bezierPath.curve(to: NSPoint(x: 4.74, y: -13.48), controlPoint1: NSPoint(x: 4.84, y: -13.75), controlPoint2: NSPoint(x: 4.81, y: -13.67)) + bezierPath.line(to: NSPoint(x: 4.74, y: -13.48)) + bezierPath.curve(to: NSPoint(x: 4.6, y: -12.8), controlPoint1: NSPoint(x: 4.57, y: -12.98), controlPoint2: NSPoint(x: 4.55, y: -12.91)) + bezierPath.curve(to: NSPoint(x: 4.86, y: -12.29), controlPoint1: NSPoint(x: 4.66, y: -12.65), controlPoint2: NSPoint(x: 4.74, y: -12.49)) + bezierPath.curve(to: NSPoint(x: 5.04, y: -11.99), controlPoint1: NSPoint(x: 4.9, y: -12.22), controlPoint2: NSPoint(x: 4.95, y: -12.14)) + bezierPath.curve(to: NSPoint(x: 5.45, y: -11.16), controlPoint1: NSPoint(x: 5.24, y: -11.65), controlPoint2: NSPoint(x: 5.36, y: -11.42)) + bezierPath.line(to: NSPoint(x: 5.46, y: -11.28)) + bezierPath.curve(to: NSPoint(x: 5.21, y: -10.98), controlPoint1: NSPoint(x: 5.4, y: -11.15), controlPoint2: NSPoint(x: 5.33, y: -11.07)) + bezierPath.curve(to: NSPoint(x: 5.04, y: -10.88), controlPoint1: NSPoint(x: 5.17, y: -10.96), controlPoint2: NSPoint(x: 5.02, y: -10.86)) + bezierPath.curve(to: NSPoint(x: 4.75, y: -10.3), controlPoint1: NSPoint(x: 4.8, y: -10.73), controlPoint2: NSPoint(x: 4.71, y: -10.57)) + bezierPath.curve(to: NSPoint(x: 5.18, y: -9.64), controlPoint1: NSPoint(x: 4.74, y: -10.03), controlPoint2: NSPoint(x: 4.88, y: -9.85)) + bezierPath.curve(to: NSPoint(x: 5.5, y: -9.43), controlPoint1: NSPoint(x: 5.2, y: -9.63), controlPoint2: NSPoint(x: 5.43, y: -9.48)) + bezierPath.curve(to: NSPoint(x: 6.01, y: -8.8), controlPoint1: NSPoint(x: 5.75, y: -9.24), controlPoint2: NSPoint(x: 5.91, y: -9.06)) + bezierPath.line(to: NSPoint(x: 6.03, y: -8.95)) + bezierPath.curve(to: NSPoint(x: 5.64, y: -3.24), controlPoint1: NSPoint(x: 4.84, y: -7.29), controlPoint2: NSPoint(x: 4.67, y: -5.09)) + bezierPath.curve(to: NSPoint(x: 11.07, y: -0.33), controlPoint1: NSPoint(x: 6.69, y: -1.22), controlPoint2: NSPoint(x: 8.86, y: -0.08)) + bezierPath.line(to: NSPoint(x: 12.09, y: -0.44)) + bezierPath.line(to: NSPoint(x: 11.09, y: -0.64)) + bezierPath.curve(to: NSPoint(x: 6.96, y: -5.68), controlPoint1: NSPoint(x: 8.71, y: -1.1), controlPoint2: NSPoint(x: 6.96, y: -3.21)) + bezierPath.curve(to: NSPoint(x: 9.78, y: -10.28), controlPoint1: NSPoint(x: 6.96, y: -7.65), controlPoint2: NSPoint(x: 8.06, y: -9.42)) + bezierPath.line(to: NSPoint(x: 9.86, y: -10.41)) + bezierPath.curve(to: NSPoint(x: 9.57, y: -11.55), controlPoint1: NSPoint(x: 9.91, y: -10.81), controlPoint2: NSPoint(x: 9.82, y: -11.11)) + bezierPath.line(to: NSPoint(x: 9.53, y: -11.6)) + bezierPath.close() + bezierPath.move(to: NSPoint(x: 9.34, y: -11.35)) + bezierPath.line(to: NSPoint(x: 9.3, y: -11.4)) + bezierPath.curve(to: NSPoint(x: 9.55, y: -10.44), controlPoint1: NSPoint(x: 9.51, y: -11.01), controlPoint2: NSPoint(x: 9.59, y: -10.77)) + bezierPath.line(to: NSPoint(x: 9.64, y: -10.56)) + bezierPath.curve(to: NSPoint(x: 6.65, y: -5.68), controlPoint1: NSPoint(x: 7.82, y: -9.65), controlPoint2: NSPoint(x: 6.65, y: -7.77)) + bezierPath.curve(to: NSPoint(x: 11.03, y: -0.33), controlPoint1: NSPoint(x: 6.65, y: -3.06), controlPoint2: NSPoint(x: 8.5, y: -0.82)) + bezierPath.line(to: NSPoint(x: 11.04, y: -0.64)) + bezierPath.curve(to: NSPoint(x: 5.91, y: -3.38), controlPoint1: NSPoint(x: 8.96, y: -0.4), controlPoint2: NSPoint(x: 6.91, y: -1.48)) + bezierPath.curve(to: NSPoint(x: 6.28, y: -8.76), controlPoint1: NSPoint(x: 5, y: -5.13), controlPoint2: NSPoint(x: 5.17, y: -7.21)) + bezierPath.line(to: NSPoint(x: 6.3, y: -8.91)) + bezierPath.curve(to: NSPoint(x: 5.68, y: -9.68), controlPoint1: NSPoint(x: 6.18, y: -9.24), controlPoint2: NSPoint(x: 5.98, y: -9.46)) + bezierPath.curve(to: NSPoint(x: 5.36, y: -9.9), controlPoint1: NSPoint(x: 5.61, y: -9.73), controlPoint2: NSPoint(x: 5.38, y: -9.89)) + bezierPath.curve(to: NSPoint(x: 5.06, y: -10.32), controlPoint1: NSPoint(x: 5.14, y: -10.05), controlPoint2: NSPoint(x: 5.06, y: -10.16)) + bezierPath.curve(to: NSPoint(x: 5.21, y: -10.62), controlPoint1: NSPoint(x: 5.04, y: -10.48), controlPoint2: NSPoint(x: 5.07, y: -10.52)) + bezierPath.curve(to: NSPoint(x: 5.38, y: -10.73), controlPoint1: NSPoint(x: 5.18, y: -10.6), controlPoint2: NSPoint(x: 5.34, y: -10.7)) + bezierPath.curve(to: NSPoint(x: 5.74, y: -11.15), controlPoint1: NSPoint(x: 5.55, y: -10.84), controlPoint2: NSPoint(x: 5.66, y: -10.97)) + bezierPath.line(to: NSPoint(x: 5.75, y: -11.27)) + bezierPath.curve(to: NSPoint(x: 5.31, y: -12.15), controlPoint1: NSPoint(x: 5.65, y: -11.55), controlPoint2: NSPoint(x: 5.52, y: -11.79)) + bezierPath.curve(to: NSPoint(x: 5.13, y: -12.45), controlPoint1: NSPoint(x: 5.22, y: -12.3), controlPoint2: NSPoint(x: 5.17, y: -12.38)) + bezierPath.curve(to: NSPoint(x: 4.88, y: -12.92), controlPoint1: NSPoint(x: 5.02, y: -12.64), controlPoint2: NSPoint(x: 4.95, y: -12.78)) + bezierPath.curve(to: NSPoint(x: 5.04, y: -13.38), controlPoint1: NSPoint(x: 4.89, y: -12.92), controlPoint2: NSPoint(x: 4.95, y: -13.12)) + bezierPath.line(to: NSPoint(x: 5.04, y: -13.38)) + bezierPath.curve(to: NSPoint(x: 5.16, y: -13.75), controlPoint1: NSPoint(x: 5.11, y: -13.57), controlPoint2: NSPoint(x: 5.13, y: -13.66)) + bezierPath.line(to: NSPoint(x: 5.14, y: -13.87)) + bezierPath.curve(to: NSPoint(x: 4.72, y: -14.59), controlPoint1: NSPoint(x: 4.93, y: -14.23), controlPoint2: NSPoint(x: 4.93, y: -14.23)) + bezierPath.line(to: NSPoint(x: 4.64, y: -14.65)) + bezierPath.curve(to: NSPoint(x: 4.63, y: -15.27), controlPoint1: NSPoint(x: 4.46, y: -14.73), controlPoint2: NSPoint(x: 4.46, y: -14.84)) + bezierPath.line(to: NSPoint(x: 4.63, y: -15.27)) + bezierPath.curve(to: NSPoint(x: 4.78, y: -15.8), controlPoint1: NSPoint(x: 4.74, y: -15.54), controlPoint2: NSPoint(x: 4.77, y: -15.66)) + bezierPath.curve(to: NSPoint(x: 4.36, y: -16.61), controlPoint1: NSPoint(x: 4.69, y: -16.16), controlPoint2: NSPoint(x: 4.55, y: -16.41)) + bezierPath.line(to: NSPoint(x: 4.3, y: -16.65)) + bezierPath.curve(to: NSPoint(x: 3.48, y: -16.95), controlPoint1: NSPoint(x: 3.89, y: -16.8), controlPoint2: NSPoint(x: 3.89, y: -16.8)) + bezierPath.line(to: NSPoint(x: 3.58, y: -16.8)) + bezierPath.curve(to: NSPoint(x: 3.58, y: -17.36), controlPoint1: NSPoint(x: 3.58, y: -17.08), controlPoint2: NSPoint(x: 3.58, y: -17.08)) + bezierPath.curve(to: NSPoint(x: 3.58, y: -17.5), controlPoint1: NSPoint(x: 3.58, y: -17.43), controlPoint2: NSPoint(x: 3.58, y: -17.43)) + bezierPath.line(to: NSPoint(x: 3.58, y: -17.5)) + bezierPath.curve(to: NSPoint(x: 3.15, y: -18.21), controlPoint1: NSPoint(x: 3.47, y: -17.86), controlPoint2: NSPoint(x: 3.33, y: -18.08)) + bezierPath.curve(to: NSPoint(x: 2.24, y: -18.6), controlPoint1: NSPoint(x: 2.68, y: -18.41), controlPoint2: NSPoint(x: 2.68, y: -18.41)) + bezierPath.line(to: NSPoint(x: 2.34, y: -18.44)) + bezierPath.curve(to: NSPoint(x: 2.38, y: -18.76), controlPoint1: NSPoint(x: 2.36, y: -18.6), controlPoint2: NSPoint(x: 2.36, y: -18.6)) + bezierPath.curve(to: NSPoint(x: 2.42, y: -19.08), controlPoint1: NSPoint(x: 2.4, y: -18.92), controlPoint2: NSPoint(x: 2.4, y: -18.92)) + bezierPath.line(to: NSPoint(x: 2.41, y: -19.15)) + bezierPath.curve(to: NSPoint(x: 1.94, y: -19.91), controlPoint1: NSPoint(x: 2.31, y: -19.45), controlPoint2: NSPoint(x: 2.16, y: -19.7)) + bezierPath.line(to: NSPoint(x: 1.88, y: -19.95)) + bezierPath.curve(to: NSPoint(x: 1.75, y: -19.99), controlPoint1: NSPoint(x: 1.81, y: -19.97), controlPoint2: NSPoint(x: 1.81, y: -19.97)) + bezierPath.curve(to: NSPoint(x: 1.49, y: -20.07), controlPoint1: NSPoint(x: 1.62, y: -20.03), controlPoint2: NSPoint(x: 1.62, y: -20.03)) + bezierPath.curve(to: NSPoint(x: 1.23, y: -20.15), controlPoint1: NSPoint(x: 1.36, y: -20.11), controlPoint2: NSPoint(x: 1.36, y: -20.11)) + bezierPath.line(to: NSPoint(x: 1.34, y: -20.01)) + bezierPath.curve(to: NSPoint(x: 2.01, y: -22.47), controlPoint1: NSPoint(x: 1.33, y: -20.93), controlPoint2: NSPoint(x: 1.51, y: -21.68)) + bezierPath.line(to: NSPoint(x: 1.83, y: -22.41)) + bezierPath.curve(to: NSPoint(x: 3.44, y: -22.36), controlPoint1: NSPoint(x: 2.23, y: -22.28), controlPoint2: NSPoint(x: 2.57, y: -22.29)) + bezierPath.line(to: NSPoint(x: 3.29, y: -22.44)) + bezierPath.curve(to: NSPoint(x: 7.35, y: -14.65), controlPoint1: NSPoint(x: 4.48, y: -20.15), controlPoint2: NSPoint(x: 5.32, y: -18.54)) + bezierPath.curve(to: NSPoint(x: 8.7, y: -12.05), controlPoint1: NSPoint(x: 8.03, y: -13.35), controlPoint2: NSPoint(x: 8.03, y: -13.35)) + bezierPath.curve(to: NSPoint(x: 9.34, y: -11.35), controlPoint1: NSPoint(x: 8.89, y: -11.77), controlPoint2: NSPoint(x: 9.1, y: -11.54)) + bezierPath.close() + setupKeyFill.setFill() + bezierPath.fill() + + + //// Bezier 2 Drawing + let bezier2Path = NSBezierPath() + bezier2Path.move(to: NSPoint(x: 12.05, y: -0.23)) + bezier2Path.curve(to: NSPoint(x: 13.14, y: -0.34), controlPoint1: NSPoint(x: 12.42, y: -0.23), controlPoint2: NSPoint(x: 12.78, y: -0.27)) + bezier2Path.line(to: NSPoint(x: 14, y: -0.52)) + bezier2Path.line(to: NSPoint(x: 13.13, y: -0.65)) + bezier2Path.curve(to: NSPoint(x: 11.51, y: -1.18), controlPoint1: NSPoint(x: 12.57, y: -0.73), controlPoint2: NSPoint(x: 12.02, y: -0.91)) + bezier2Path.curve(to: NSPoint(x: 9.42, y: -8.11), controlPoint1: NSPoint(x: 9.02, y: -2.5), controlPoint2: NSPoint(x: 8.09, y: -5.6)) + bezier2Path.curve(to: NSPoint(x: 13.94, y: -10.86), controlPoint1: NSPoint(x: 10.32, y: -9.81), controlPoint2: NSPoint(x: 12.06, y: -10.85)) + bezier2Path.line(to: NSPoint(x: 14.2, y: -10.86)) + bezier2Path.line(to: NSPoint(x: 14.07, y: -11.09)) + bezier2Path.curve(to: NSPoint(x: 13.75, y: -12.1), controlPoint1: NSPoint(x: 13.9, y: -11.39), controlPoint2: NSPoint(x: 13.79, y: -11.73)) + bezier2Path.curve(to: NSPoint(x: 13.75, y: -23.8), controlPoint1: NSPoint(x: 13.75, y: -17.94), controlPoint2: NSPoint(x: 13.75, y: -17.94)) + bezier2Path.line(to: NSPoint(x: 13.66, y: -23.94)) + bezier2Path.curve(to: NSPoint(x: 13.47, y: -24.02), controlPoint1: NSPoint(x: 13.56, y: -23.98), controlPoint2: NSPoint(x: 13.56, y: -23.98)) + bezier2Path.curve(to: NSPoint(x: 12.35, y: -24.66), controlPoint1: NSPoint(x: 12.89, y: -24.27), controlPoint2: NSPoint(x: 12.59, y: -24.43)) + bezier2Path.line(to: NSPoint(x: 12.17, y: -24.68)) + bezier2Path.curve(to: NSPoint(x: 10.31, y: -22.68), controlPoint1: NSPoint(x: 11.31, y: -24.18), controlPoint2: NSPoint(x: 10.76, y: -23.56)) + bezier2Path.line(to: NSPoint(x: 10.34, y: -22.5)) + bezier2Path.curve(to: NSPoint(x: 10.58, y: -22.26), controlPoint1: NSPoint(x: 10.46, y: -22.38), controlPoint2: NSPoint(x: 10.46, y: -22.38)) + bezier2Path.curve(to: NSPoint(x: 10.82, y: -22.02), controlPoint1: NSPoint(x: 10.7, y: -22.14), controlPoint2: NSPoint(x: 10.7, y: -22.14)) + bezier2Path.line(to: NSPoint(x: 10.78, y: -22.08)) + bezier2Path.curve(to: NSPoint(x: 10.84, y: -21.33), controlPoint1: NSPoint(x: 10.86, y: -21.84), controlPoint2: NSPoint(x: 10.88, y: -21.59)) + bezier2Path.line(to: NSPoint(x: 10.86, y: -21.4)) + bezier2Path.curve(to: NSPoint(x: 10.49, y: -20.87), controlPoint1: NSPoint(x: 10.68, y: -21.13), controlPoint2: NSPoint(x: 10.68, y: -21.13)) + bezier2Path.line(to: NSPoint(x: 10.5, y: -20.68)) + bezier2Path.curve(to: NSPoint(x: 10.74, y: -20.38), controlPoint1: NSPoint(x: 10.62, y: -20.53), controlPoint2: NSPoint(x: 10.62, y: -20.53)) + bezier2Path.curve(to: NSPoint(x: 10.98, y: -20.08), controlPoint1: NSPoint(x: 10.86, y: -20.23), controlPoint2: NSPoint(x: 10.86, y: -20.23)) + bezier2Path.curve(to: NSPoint(x: 11.11, y: -19.93), controlPoint1: NSPoint(x: 11.05, y: -20.01), controlPoint2: NSPoint(x: 11.05, y: -20.01)) + bezier2Path.curve(to: NSPoint(x: 11.13, y: -19.37), controlPoint1: NSPoint(x: 11.15, y: -19.84), controlPoint2: NSPoint(x: 11.17, y: -19.64)) + bezier2Path.line(to: NSPoint(x: 11.14, y: -19.42)) + bezier2Path.curve(to: NSPoint(x: 10.91, y: -18.98), controlPoint1: NSPoint(x: 11.03, y: -19.2), controlPoint2: NSPoint(x: 11.03, y: -19.2)) + bezier2Path.curve(to: NSPoint(x: 10.82, y: -18.81), controlPoint1: NSPoint(x: 10.86, y: -18.9), controlPoint2: NSPoint(x: 10.86, y: -18.9)) + bezier2Path.line(to: NSPoint(x: 10.84, y: -18.63)) + bezier2Path.curve(to: NSPoint(x: 11.43, y: -17.98), controlPoint1: NSPoint(x: 11.13, y: -18.31), controlPoint2: NSPoint(x: 11.13, y: -18.31)) + bezier2Path.line(to: NSPoint(x: 11.4, y: -18.04)) + bezier2Path.curve(to: NSPoint(x: 11.41, y: -17.33), controlPoint1: NSPoint(x: 11.46, y: -17.83), controlPoint2: NSPoint(x: 11.46, y: -17.59)) + bezier2Path.curve(to: NSPoint(x: 11.12, y: -17.05), controlPoint1: NSPoint(x: 11.38, y: -17.29), controlPoint2: NSPoint(x: 11.3, y: -17.22)) + bezier2Path.line(to: NSPoint(x: 11.12, y: -17.05)) + bezier2Path.curve(to: NSPoint(x: 10.8, y: -16.06), controlPoint1: NSPoint(x: 10.65, y: -16.65), controlPoint2: NSPoint(x: 10.51, y: -16.41)) + bezier2Path.line(to: NSPoint(x: 10.77, y: -16.15)) + bezier2Path.curve(to: NSPoint(x: 10.79, y: -15.74), controlPoint1: NSPoint(x: 10.78, y: -15.95), controlPoint2: NSPoint(x: 10.78, y: -15.95)) + bezier2Path.curve(to: NSPoint(x: 10.81, y: -15.32), controlPoint1: NSPoint(x: 10.8, y: -15.53), controlPoint2: NSPoint(x: 10.8, y: -15.53)) + bezier2Path.line(to: NSPoint(x: 10.86, y: -15.44)) + bezier2Path.curve(to: NSPoint(x: 10.59, y: -15.18), controlPoint1: NSPoint(x: 10.8, y: -15.38), controlPoint2: NSPoint(x: 10.74, y: -15.32)) + bezier2Path.line(to: NSPoint(x: 10.59, y: -15.18)) + bezier2Path.curve(to: NSPoint(x: 10.15, y: -14.64), controlPoint1: NSPoint(x: 10.2, y: -14.81), controlPoint2: NSPoint(x: 10.15, y: -14.76)) + bezier2Path.curve(to: NSPoint(x: 10.16, y: -13.83), controlPoint1: NSPoint(x: 10.13, y: -14.42), controlPoint2: NSPoint(x: 10.13, y: -14.24)) + bezier2Path.curve(to: NSPoint(x: 10.15, y: -12.79), controlPoint1: NSPoint(x: 10.19, y: -13.33), controlPoint2: NSPoint(x: 10.19, y: -13.09)) + bezier2Path.line(to: NSPoint(x: 10.21, y: -12.89)) + bezier2Path.curve(to: NSPoint(x: 10.2, y: -12.89), controlPoint1: NSPoint(x: 10.2, y: -12.89), controlPoint2: NSPoint(x: 10.2, y: -12.89)) + bezier2Path.curve(to: NSPoint(x: 9.71, y: -12.74), controlPoint1: NSPoint(x: 10.07, y: -12.78), controlPoint2: NSPoint(x: 9.96, y: -12.76)) + bezier2Path.curve(to: NSPoint(x: 9.13, y: -12.35), controlPoint1: NSPoint(x: 9.38, y: -12.71), controlPoint2: NSPoint(x: 9.22, y: -12.64)) + bezier2Path.curve(to: NSPoint(x: 9.06, y: -11.98), controlPoint1: NSPoint(x: 9.07, y: -12.25), controlPoint2: NSPoint(x: 9.04, y: -12.12)) + bezier2Path.curve(to: NSPoint(x: 9.33, y: -11.35), controlPoint1: NSPoint(x: 9.08, y: -11.81), controlPoint2: NSPoint(x: 9.08, y: -11.8)) + bezier2Path.curve(to: NSPoint(x: 9.55, y: -10.44), controlPoint1: NSPoint(x: 9.52, y: -11), controlPoint2: NSPoint(x: 9.59, y: -10.76)) + bezier2Path.line(to: NSPoint(x: 9.64, y: -10.56)) + bezier2Path.curve(to: NSPoint(x: 6.65, y: -5.68), controlPoint1: NSPoint(x: 7.82, y: -9.65), controlPoint2: NSPoint(x: 6.65, y: -7.77)) + bezier2Path.curve(to: NSPoint(x: 12.05, y: -0.23), controlPoint1: NSPoint(x: 6.65, y: -2.67), controlPoint2: NSPoint(x: 9.07, y: -0.23)) + bezier2Path.close() + bezier2Path.move(to: NSPoint(x: 12.05, y: -0.54)) + bezier2Path.curve(to: NSPoint(x: 6.96, y: -5.68), controlPoint1: NSPoint(x: 9.24, y: -0.54), controlPoint2: NSPoint(x: 6.96, y: -2.85)) + bezier2Path.curve(to: NSPoint(x: 9.78, y: -10.28), controlPoint1: NSPoint(x: 6.96, y: -7.65), controlPoint2: NSPoint(x: 8.06, y: -9.42)) + bezier2Path.line(to: NSPoint(x: 9.86, y: -10.41)) + bezier2Path.curve(to: NSPoint(x: 9.6, y: -11.5), controlPoint1: NSPoint(x: 9.91, y: -10.8), controlPoint2: NSPoint(x: 9.82, y: -11.1)) + bezier2Path.curve(to: NSPoint(x: 9.37, y: -12.02), controlPoint1: NSPoint(x: 9.39, y: -11.89), controlPoint2: NSPoint(x: 9.38, y: -11.9)) + bezier2Path.curve(to: NSPoint(x: 9.41, y: -12.23), controlPoint1: NSPoint(x: 9.36, y: -12.09), controlPoint2: NSPoint(x: 9.37, y: -12.16)) + bezier2Path.curve(to: NSPoint(x: 9.74, y: -12.43), controlPoint1: NSPoint(x: 9.47, y: -12.39), controlPoint2: NSPoint(x: 9.51, y: -12.41)) + bezier2Path.curve(to: NSPoint(x: 10.39, y: -12.64), controlPoint1: NSPoint(x: 10.04, y: -12.45), controlPoint2: NSPoint(x: 10.2, y: -12.49)) + bezier2Path.curve(to: NSPoint(x: 10.4, y: -12.65), controlPoint1: NSPoint(x: 10.4, y: -12.65), controlPoint2: NSPoint(x: 10.4, y: -12.65)) + bezier2Path.line(to: NSPoint(x: 10.46, y: -12.75)) + bezier2Path.curve(to: NSPoint(x: 10.47, y: -13.85), controlPoint1: NSPoint(x: 10.5, y: -13.08), controlPoint2: NSPoint(x: 10.5, y: -13.33)) + bezier2Path.curve(to: NSPoint(x: 10.46, y: -14.62), controlPoint1: NSPoint(x: 10.45, y: -14.25), controlPoint2: NSPoint(x: 10.44, y: -14.41)) + bezier2Path.curve(to: NSPoint(x: 10.81, y: -14.95), controlPoint1: NSPoint(x: 10.46, y: -14.61), controlPoint2: NSPoint(x: 10.61, y: -14.77)) + bezier2Path.line(to: NSPoint(x: 10.81, y: -14.95)) + bezier2Path.curve(to: NSPoint(x: 11.08, y: -15.22), controlPoint1: NSPoint(x: 10.96, y: -15.09), controlPoint2: NSPoint(x: 11.02, y: -15.15)) + bezier2Path.line(to: NSPoint(x: 11.13, y: -15.34)) + bezier2Path.curve(to: NSPoint(x: 11.1, y: -15.75), controlPoint1: NSPoint(x: 11.11, y: -15.55), controlPoint2: NSPoint(x: 11.11, y: -15.55)) + bezier2Path.curve(to: NSPoint(x: 11.08, y: -16.17), controlPoint1: NSPoint(x: 11.09, y: -15.96), controlPoint2: NSPoint(x: 11.09, y: -15.96)) + bezier2Path.line(to: NSPoint(x: 11.04, y: -16.26)) + bezier2Path.curve(to: NSPoint(x: 11.32, y: -16.82), controlPoint1: NSPoint(x: 10.92, y: -16.42), controlPoint2: NSPoint(x: 10.97, y: -16.52)) + bezier2Path.line(to: NSPoint(x: 11.32, y: -16.82)) + bezier2Path.curve(to: NSPoint(x: 11.7, y: -17.22), controlPoint1: NSPoint(x: 11.54, y: -17.01), controlPoint2: NSPoint(x: 11.63, y: -17.09)) + bezier2Path.curve(to: NSPoint(x: 11.69, y: -18.13), controlPoint1: NSPoint(x: 11.78, y: -17.58), controlPoint2: NSPoint(x: 11.77, y: -17.86)) + bezier2Path.line(to: NSPoint(x: 11.66, y: -18.19)) + bezier2Path.curve(to: NSPoint(x: 11.07, y: -18.84), controlPoint1: NSPoint(x: 11.37, y: -18.52), controlPoint2: NSPoint(x: 11.37, y: -18.52)) + bezier2Path.line(to: NSPoint(x: 11.09, y: -18.66)) + bezier2Path.curve(to: NSPoint(x: 11.19, y: -18.84), controlPoint1: NSPoint(x: 11.14, y: -18.75), controlPoint2: NSPoint(x: 11.14, y: -18.75)) + bezier2Path.curve(to: NSPoint(x: 11.42, y: -19.28), controlPoint1: NSPoint(x: 11.3, y: -19.06), controlPoint2: NSPoint(x: 11.3, y: -19.06)) + bezier2Path.line(to: NSPoint(x: 11.42, y: -19.28)) + bezier2Path.curve(to: NSPoint(x: 11.36, y: -20.1), controlPoint1: NSPoint(x: 11.49, y: -19.65), controlPoint2: NSPoint(x: 11.47, y: -19.91)) + bezier2Path.curve(to: NSPoint(x: 11.23, y: -20.28), controlPoint1: NSPoint(x: 11.29, y: -20.2), controlPoint2: NSPoint(x: 11.29, y: -20.2)) + bezier2Path.curve(to: NSPoint(x: 10.99, y: -20.58), controlPoint1: NSPoint(x: 11.11, y: -20.43), controlPoint2: NSPoint(x: 11.11, y: -20.43)) + bezier2Path.curve(to: NSPoint(x: 10.74, y: -20.88), controlPoint1: NSPoint(x: 10.86, y: -20.73), controlPoint2: NSPoint(x: 10.86, y: -20.73)) + bezier2Path.line(to: NSPoint(x: 10.75, y: -20.69)) + bezier2Path.curve(to: NSPoint(x: 11.12, y: -21.22), controlPoint1: NSPoint(x: 10.93, y: -20.96), controlPoint2: NSPoint(x: 10.93, y: -20.96)) + bezier2Path.line(to: NSPoint(x: 11.15, y: -21.29)) + bezier2Path.curve(to: NSPoint(x: 11.08, y: -22.18), controlPoint1: NSPoint(x: 11.19, y: -21.59), controlPoint2: NSPoint(x: 11.17, y: -21.89)) + bezier2Path.line(to: NSPoint(x: 11.04, y: -22.24)) + bezier2Path.curve(to: NSPoint(x: 10.8, y: -22.48), controlPoint1: NSPoint(x: 10.92, y: -22.36), controlPoint2: NSPoint(x: 10.92, y: -22.36)) + bezier2Path.curve(to: NSPoint(x: 10.56, y: -22.72), controlPoint1: NSPoint(x: 10.68, y: -22.6), controlPoint2: NSPoint(x: 10.68, y: -22.6)) + bezier2Path.line(to: NSPoint(x: 10.59, y: -22.54)) + bezier2Path.curve(to: NSPoint(x: 12.32, y: -24.42), controlPoint1: NSPoint(x: 11.01, y: -23.36), controlPoint2: NSPoint(x: 11.52, y: -23.94)) + bezier2Path.line(to: NSPoint(x: 12.14, y: -24.44)) + bezier2Path.curve(to: NSPoint(x: 13.35, y: -23.73), controlPoint1: NSPoint(x: 12.4, y: -24.18), controlPoint2: NSPoint(x: 12.73, y: -23.99)) + bezier2Path.curve(to: NSPoint(x: 13.54, y: -23.65), controlPoint1: NSPoint(x: 13.44, y: -23.69), controlPoint2: NSPoint(x: 13.44, y: -23.69)) + bezier2Path.line(to: NSPoint(x: 13.44, y: -23.8)) + bezier2Path.curve(to: NSPoint(x: 13.44, y: -12.08), controlPoint1: NSPoint(x: 13.44, y: -17.94), controlPoint2: NSPoint(x: 13.44, y: -17.94)) + bezier2Path.curve(to: NSPoint(x: 13.8, y: -10.93), controlPoint1: NSPoint(x: 13.49, y: -11.66), controlPoint2: NSPoint(x: 13.61, y: -11.28)) + bezier2Path.line(to: NSPoint(x: 13.93, y: -11.17)) + bezier2Path.curve(to: NSPoint(x: 9.14, y: -8.25), controlPoint1: NSPoint(x: 11.95, y: -11.16), controlPoint2: NSPoint(x: 10.1, y: -10.06)) + bezier2Path.curve(to: NSPoint(x: 11.36, y: -0.91), controlPoint1: NSPoint(x: 7.73, y: -5.59), controlPoint2: NSPoint(x: 8.73, y: -2.3)) + bezier2Path.curve(to: NSPoint(x: 13.08, y: -0.34), controlPoint1: NSPoint(x: 11.91, y: -0.62), controlPoint2: NSPoint(x: 12.49, y: -0.43)) + bezier2Path.line(to: NSPoint(x: 13.07, y: -0.65)) + bezier2Path.curve(to: NSPoint(x: 12.05, y: -0.54), controlPoint1: NSPoint(x: 12.74, y: -0.58), controlPoint2: NSPoint(x: 12.4, y: -0.54)) + bezier2Path.close() + setupKeyFill.setFill() + bezier2Path.fill() + + + //// Bezier 3 Drawing + let bezier3Path = NSBezierPath() + bezier3Path.move(to: NSPoint(x: 11.36, y: -0.91)) + bezier3Path.curve(to: NSPoint(x: 18.69, y: -3.2), controlPoint1: NSPoint(x: 14, y: 0.49), controlPoint2: NSPoint(x: 17.28, y: -0.54)) + bezier3Path.curve(to: NSPoint(x: 18.31, y: -8.94), controlPoint1: NSPoint(x: 19.67, y: -5.05), controlPoint2: NSPoint(x: 19.51, y: -7.27)) + bezier3Path.curve(to: NSPoint(x: 18.42, y: -10.59), controlPoint1: NSPoint(x: 18.17, y: -9.47), controlPoint2: NSPoint(x: 18.2, y: -10.04)) + bezier3Path.curve(to: NSPoint(x: 21.15, y: -15.76), controlPoint1: NSPoint(x: 19.78, y: -13.17), controlPoint2: NSPoint(x: 19.78, y: -13.17)) + bezier3Path.curve(to: NSPoint(x: 21.55, y: -16.5), controlPoint1: NSPoint(x: 21.35, y: -16.13), controlPoint2: NSPoint(x: 21.35, y: -16.13)) + bezier3Path.curve(to: NSPoint(x: 23.9, y: -20.93), controlPoint1: NSPoint(x: 22.72, y: -18.72), controlPoint2: NSPoint(x: 22.72, y: -18.72)) + bezier3Path.line(to: NSPoint(x: 23.88, y: -21.11)) + bezier3Path.curve(to: NSPoint(x: 23.34, y: -21.8), controlPoint1: NSPoint(x: 23.59, y: -21.46), controlPoint2: NSPoint(x: 23.47, y: -21.61)) + bezier3Path.curve(to: NSPoint(x: 23.07, y: -22.35), controlPoint1: NSPoint(x: 23.21, y: -22), controlPoint2: NSPoint(x: 23.12, y: -22.18)) + bezier3Path.line(to: NSPoint(x: 22.91, y: -22.46)) + bezier3Path.curve(to: NSPoint(x: 20.33, y: -21.56), controlPoint1: NSPoint(x: 21.91, y: -22.42), controlPoint2: NSPoint(x: 21.14, y: -22.12)) + bezier3Path.line(to: NSPoint(x: 20.27, y: -21.39)) + bezier3Path.curve(to: NSPoint(x: 20.41, y: -20.95), controlPoint1: NSPoint(x: 20.34, y: -21.17), controlPoint2: NSPoint(x: 20.34, y: -21.17)) + bezier3Path.curve(to: NSPoint(x: 20.47, y: -20.73), controlPoint1: NSPoint(x: 20.44, y: -20.84), controlPoint2: NSPoint(x: 20.44, y: -20.84)) + bezier3Path.line(to: NSPoint(x: 20.47, y: -20.81)) + bezier3Path.curve(to: NSPoint(x: 20.17, y: -20.12), controlPoint1: NSPoint(x: 20.43, y: -20.56), controlPoint2: NSPoint(x: 20.33, y: -20.33)) + bezier3Path.line(to: NSPoint(x: 20.22, y: -20.17)) + bezier3Path.curve(to: NSPoint(x: 19.93, y: -20.02), controlPoint1: NSPoint(x: 20.08, y: -20.09), controlPoint2: NSPoint(x: 20.08, y: -20.09)) + bezier3Path.curve(to: NSPoint(x: 19.65, y: -19.87), controlPoint1: NSPoint(x: 19.79, y: -19.95), controlPoint2: NSPoint(x: 19.79, y: -19.95)) + bezier3Path.line(to: NSPoint(x: 19.56, y: -19.7)) + bezier3Path.curve(to: NSPoint(x: 19.66, y: -19.23), controlPoint1: NSPoint(x: 19.61, y: -19.47), controlPoint2: NSPoint(x: 19.61, y: -19.47)) + bezier3Path.curve(to: NSPoint(x: 19.68, y: -19.14), controlPoint1: NSPoint(x: 19.67, y: -19.18), controlPoint2: NSPoint(x: 19.67, y: -19.18)) + bezier3Path.curve(to: NSPoint(x: 19.75, y: -18.76), controlPoint1: NSPoint(x: 19.71, y: -18.95), controlPoint2: NSPoint(x: 19.71, y: -18.95)) + bezier3Path.curve(to: NSPoint(x: 19.51, y: -18.25), controlPoint1: NSPoint(x: 19.75, y: -18.65), controlPoint2: NSPoint(x: 19.67, y: -18.47)) + bezier3Path.curve(to: NSPoint(x: 19.26, y: -18.1), controlPoint1: NSPoint(x: 19.4, y: -18.19), controlPoint2: NSPoint(x: 19.4, y: -18.19)) + bezier3Path.curve(to: NSPoint(x: 18.97, y: -17.9), controlPoint1: NSPoint(x: 19.11, y: -18), controlPoint2: NSPoint(x: 19.11, y: -18)) + bezier3Path.line(to: NSPoint(x: 18.9, y: -17.73)) + bezier3Path.curve(to: NSPoint(x: 19.12, y: -16.88), controlPoint1: NSPoint(x: 19.01, y: -17.31), controlPoint2: NSPoint(x: 19.01, y: -17.31)) + bezier3Path.line(to: NSPoint(x: 19.12, y: -16.95)) + bezier3Path.curve(to: NSPoint(x: 18.8, y: -16.32), controlPoint1: NSPoint(x: 19.08, y: -16.73), controlPoint2: NSPoint(x: 18.97, y: -16.52)) + bezier3Path.curve(to: NSPoint(x: 18.41, y: -16.21), controlPoint1: NSPoint(x: 18.75, y: -16.3), controlPoint2: NSPoint(x: 18.65, y: -16.27)) + bezier3Path.curve(to: NSPoint(x: 17.67, y: -15.48), controlPoint1: NSPoint(x: 17.8, y: -16.07), controlPoint2: NSPoint(x: 17.58, y: -15.92)) + bezier3Path.line(to: NSPoint(x: 17.68, y: -15.58)) + bezier3Path.curve(to: NSPoint(x: 17.6, y: -15.39), controlPoint1: NSPoint(x: 17.64, y: -15.48), controlPoint2: NSPoint(x: 17.64, y: -15.48)) + bezier3Path.curve(to: NSPoint(x: 17.33, y: -14.82), controlPoint1: NSPoint(x: 17.46, y: -15.1), controlPoint2: NSPoint(x: 17.46, y: -15.1)) + bezier3Path.line(to: NSPoint(x: 17.43, y: -14.9)) + bezier3Path.curve(to: NSPoint(x: 17.07, y: -14.8), controlPoint1: NSPoint(x: 17.34, y: -14.88), controlPoint2: NSPoint(x: 17.26, y: -14.85)) + bezier3Path.line(to: NSPoint(x: 17.07, y: -14.8)) + bezier3Path.curve(to: NSPoint(x: 16.42, y: -14.53), controlPoint1: NSPoint(x: 16.55, y: -14.66), controlPoint2: NSPoint(x: 16.48, y: -14.63)) + bezier3Path.curve(to: NSPoint(x: 16.09, y: -13.88), controlPoint1: NSPoint(x: 16.32, y: -14.36), controlPoint2: NSPoint(x: 16.23, y: -14.19)) + bezier3Path.curve(to: NSPoint(x: 15.79, y: -13.25), controlPoint1: NSPoint(x: 15.91, y: -13.49), controlPoint2: NSPoint(x: 15.88, y: -13.42)) + bezier3Path.curve(to: NSPoint(x: 15.56, y: -12.9), controlPoint1: NSPoint(x: 15.72, y: -13.13), controlPoint2: NSPoint(x: 15.64, y: -13.01)) + bezier3Path.line(to: NSPoint(x: 15.66, y: -12.96)) + bezier3Path.curve(to: NSPoint(x: 15.19, y: -13.03), controlPoint1: NSPoint(x: 15.5, y: -12.93), controlPoint2: NSPoint(x: 15.38, y: -12.95)) + bezier3Path.curve(to: NSPoint(x: 14.85, y: -13.14), controlPoint1: NSPoint(x: 14.98, y: -13.13), controlPoint2: NSPoint(x: 14.95, y: -13.14)) + bezier3Path.curve(to: NSPoint(x: 14.45, y: -12.98), controlPoint1: NSPoint(x: 14.71, y: -13.16), controlPoint2: NSPoint(x: 14.58, y: -13.11)) + bezier3Path.curve(to: NSPoint(x: 14.16, y: -12.1), controlPoint1: NSPoint(x: 14.2, y: -12.82), controlPoint2: NSPoint(x: 14.15, y: -12.62)) + bezier3Path.curve(to: NSPoint(x: 13.94, y: -11.11), controlPoint1: NSPoint(x: 14.17, y: -11.62), controlPoint2: NSPoint(x: 14.13, y: -11.39)) + bezier3Path.curve(to: NSPoint(x: 13.93, y: -11.1), controlPoint1: NSPoint(x: 13.94, y: -11.11), controlPoint2: NSPoint(x: 13.94, y: -11.11)) + bezier3Path.line(to: NSPoint(x: 14.06, y: -11.17)) + bezier3Path.curve(to: NSPoint(x: 9.14, y: -8.25), controlPoint1: NSPoint(x: 12.03, y: -11.21), controlPoint2: NSPoint(x: 10.12, y: -10.1)) + bezier3Path.curve(to: NSPoint(x: 11.36, y: -0.91), controlPoint1: NSPoint(x: 7.73, y: -5.59), controlPoint2: NSPoint(x: 8.73, y: -2.3)) + bezier3Path.close() + bezier3Path.move(to: NSPoint(x: 11.51, y: -1.18)) + bezier3Path.curve(to: NSPoint(x: 9.42, y: -8.11), controlPoint1: NSPoint(x: 9.02, y: -2.5), controlPoint2: NSPoint(x: 8.09, y: -5.6)) + bezier3Path.curve(to: NSPoint(x: 14.06, y: -10.85), controlPoint1: NSPoint(x: 10.34, y: -9.85), controlPoint2: NSPoint(x: 12.14, y: -10.89)) + bezier3Path.line(to: NSPoint(x: 14.19, y: -10.92)) + bezier3Path.curve(to: NSPoint(x: 14.2, y: -10.94), controlPoint1: NSPoint(x: 14.2, y: -10.93), controlPoint2: NSPoint(x: 14.2, y: -10.93)) + bezier3Path.curve(to: NSPoint(x: 14.47, y: -12.11), controlPoint1: NSPoint(x: 14.43, y: -11.28), controlPoint2: NSPoint(x: 14.48, y: -11.57)) + bezier3Path.curve(to: NSPoint(x: 14.64, y: -12.74), controlPoint1: NSPoint(x: 14.46, y: -12.52), controlPoint2: NSPoint(x: 14.49, y: -12.64)) + bezier3Path.curve(to: NSPoint(x: 14.83, y: -12.83), controlPoint1: NSPoint(x: 14.73, y: -12.82), controlPoint2: NSPoint(x: 14.78, y: -12.84)) + bezier3Path.curve(to: NSPoint(x: 15.07, y: -12.75), controlPoint1: NSPoint(x: 14.88, y: -12.83), controlPoint2: NSPoint(x: 14.9, y: -12.82)) + bezier3Path.curve(to: NSPoint(x: 15.72, y: -12.65), controlPoint1: NSPoint(x: 15.31, y: -12.64), controlPoint2: NSPoint(x: 15.49, y: -12.61)) + bezier3Path.line(to: NSPoint(x: 15.81, y: -12.71)) + bezier3Path.curve(to: NSPoint(x: 16.06, y: -13.1), controlPoint1: NSPoint(x: 15.9, y: -12.84), controlPoint2: NSPoint(x: 15.98, y: -12.96)) + bezier3Path.curve(to: NSPoint(x: 16.37, y: -13.76), controlPoint1: NSPoint(x: 16.16, y: -13.28), controlPoint2: NSPoint(x: 16.19, y: -13.35)) + bezier3Path.curve(to: NSPoint(x: 16.69, y: -14.37), controlPoint1: NSPoint(x: 16.51, y: -14.04), controlPoint2: NSPoint(x: 16.59, y: -14.2)) + bezier3Path.curve(to: NSPoint(x: 17.15, y: -14.5), controlPoint1: NSPoint(x: 16.69, y: -14.36), controlPoint2: NSPoint(x: 16.89, y: -14.43)) + bezier3Path.line(to: NSPoint(x: 17.15, y: -14.5)) + bezier3Path.curve(to: NSPoint(x: 17.52, y: -14.61), controlPoint1: NSPoint(x: 17.35, y: -14.55), controlPoint2: NSPoint(x: 17.44, y: -14.58)) + bezier3Path.line(to: NSPoint(x: 17.62, y: -14.69)) + bezier3Path.curve(to: NSPoint(x: 17.88, y: -15.26), controlPoint1: NSPoint(x: 17.75, y: -14.97), controlPoint2: NSPoint(x: 17.75, y: -14.97)) + bezier3Path.curve(to: NSPoint(x: 17.97, y: -15.45), controlPoint1: NSPoint(x: 17.92, y: -15.35), controlPoint2: NSPoint(x: 17.92, y: -15.35)) + bezier3Path.line(to: NSPoint(x: 17.98, y: -15.54)) + bezier3Path.curve(to: NSPoint(x: 18.48, y: -15.91), controlPoint1: NSPoint(x: 17.94, y: -15.74), controlPoint2: NSPoint(x: 18.03, y: -15.8)) + bezier3Path.curve(to: NSPoint(x: 19, y: -16.09), controlPoint1: NSPoint(x: 18.76, y: -15.97), controlPoint2: NSPoint(x: 18.88, y: -16.01)) + bezier3Path.curve(to: NSPoint(x: 19.43, y: -16.89), controlPoint1: NSPoint(x: 19.24, y: -16.36), controlPoint2: NSPoint(x: 19.37, y: -16.62)) + bezier3Path.line(to: NSPoint(x: 19.42, y: -16.96)) + bezier3Path.curve(to: NSPoint(x: 19.21, y: -17.81), controlPoint1: NSPoint(x: 19.32, y: -17.38), controlPoint2: NSPoint(x: 19.32, y: -17.38)) + bezier3Path.line(to: NSPoint(x: 19.14, y: -17.64)) + bezier3Path.curve(to: NSPoint(x: 19.43, y: -17.84), controlPoint1: NSPoint(x: 19.29, y: -17.74), controlPoint2: NSPoint(x: 19.29, y: -17.74)) + bezier3Path.curve(to: NSPoint(x: 19.72, y: -18.03), controlPoint1: NSPoint(x: 19.57, y: -17.93), controlPoint2: NSPoint(x: 19.57, y: -17.93)) + bezier3Path.curve(to: NSPoint(x: 20.06, y: -18.79), controlPoint1: NSPoint(x: 19.95, y: -18.33), controlPoint2: NSPoint(x: 20.06, y: -18.57)) + bezier3Path.curve(to: NSPoint(x: 19.98, y: -19.2), controlPoint1: NSPoint(x: 20.02, y: -19.01), controlPoint2: NSPoint(x: 20.02, y: -19.01)) + bezier3Path.curve(to: NSPoint(x: 19.96, y: -19.29), controlPoint1: NSPoint(x: 19.97, y: -19.24), controlPoint2: NSPoint(x: 19.97, y: -19.24)) + bezier3Path.curve(to: NSPoint(x: 19.87, y: -19.76), controlPoint1: NSPoint(x: 19.92, y: -19.53), controlPoint2: NSPoint(x: 19.92, y: -19.53)) + bezier3Path.line(to: NSPoint(x: 19.79, y: -19.59)) + bezier3Path.curve(to: NSPoint(x: 20.08, y: -19.74), controlPoint1: NSPoint(x: 19.93, y: -19.67), controlPoint2: NSPoint(x: 19.93, y: -19.67)) + bezier3Path.curve(to: NSPoint(x: 20.36, y: -19.89), controlPoint1: NSPoint(x: 20.22, y: -19.82), controlPoint2: NSPoint(x: 20.22, y: -19.82)) + bezier3Path.line(to: NSPoint(x: 20.42, y: -19.94)) + bezier3Path.curve(to: NSPoint(x: 20.78, y: -20.75), controlPoint1: NSPoint(x: 20.6, y: -20.19), controlPoint2: NSPoint(x: 20.72, y: -20.46)) + bezier3Path.line(to: NSPoint(x: 20.77, y: -20.82)) + bezier3Path.curve(to: NSPoint(x: 20.7, y: -21.04), controlPoint1: NSPoint(x: 20.74, y: -20.93), controlPoint2: NSPoint(x: 20.74, y: -20.93)) + bezier3Path.curve(to: NSPoint(x: 20.57, y: -21.48), controlPoint1: NSPoint(x: 20.64, y: -21.26), controlPoint2: NSPoint(x: 20.64, y: -21.26)) + bezier3Path.line(to: NSPoint(x: 20.51, y: -21.31)) + bezier3Path.curve(to: NSPoint(x: 22.92, y: -22.15), controlPoint1: NSPoint(x: 21.27, y: -21.83), controlPoint2: NSPoint(x: 21.99, y: -22.11)) + bezier3Path.line(to: NSPoint(x: 22.77, y: -22.26)) + bezier3Path.curve(to: NSPoint(x: 23.08, y: -21.63), controlPoint1: NSPoint(x: 22.83, y: -22.06), controlPoint2: NSPoint(x: 22.94, y: -21.85)) + bezier3Path.curve(to: NSPoint(x: 23.64, y: -20.91), controlPoint1: NSPoint(x: 23.22, y: -21.43), controlPoint2: NSPoint(x: 23.34, y: -21.28)) + bezier3Path.line(to: NSPoint(x: 23.62, y: -21.08)) + bezier3Path.curve(to: NSPoint(x: 21.27, y: -16.64), controlPoint1: NSPoint(x: 22.45, y: -18.86), controlPoint2: NSPoint(x: 22.45, y: -18.86)) + bezier3Path.curve(to: NSPoint(x: 20.88, y: -15.9), controlPoint1: NSPoint(x: 21.07, y: -16.27), controlPoint2: NSPoint(x: 21.07, y: -16.27)) + bezier3Path.curve(to: NSPoint(x: 18.14, y: -10.72), controlPoint1: NSPoint(x: 19.51, y: -13.31), controlPoint2: NSPoint(x: 19.51, y: -13.31)) + bezier3Path.curve(to: NSPoint(x: 18.03, y: -8.8), controlPoint1: NSPoint(x: 17.89, y: -10.09), controlPoint2: NSPoint(x: 17.85, y: -9.45)) + bezier3Path.curve(to: NSPoint(x: 18.41, y: -3.34), controlPoint1: NSPoint(x: 19.18, y: -7.19), controlPoint2: NSPoint(x: 19.34, y: -5.09)) + bezier3Path.curve(to: NSPoint(x: 11.51, y: -1.18), controlPoint1: NSPoint(x: 17.08, y: -0.83), controlPoint2: NSPoint(x: 13.99, y: 0.13)) + bezier3Path.close() + bezier3Path.move(to: NSPoint(x: 13.31, y: -2.09)) + bezier3Path.curve(to: NSPoint(x: 15, y: -3.78), controlPoint1: NSPoint(x: 14.25, y: -2.09), controlPoint2: NSPoint(x: 15, y: -2.85)) + bezier3Path.curve(to: NSPoint(x: 13.31, y: -5.47), controlPoint1: NSPoint(x: 15, y: -4.72), controlPoint2: NSPoint(x: 14.25, y: -5.47)) + bezier3Path.curve(to: NSPoint(x: 11.62, y: -3.78), controlPoint1: NSPoint(x: 12.38, y: -5.47), controlPoint2: NSPoint(x: 11.62, y: -4.72)) + bezier3Path.curve(to: NSPoint(x: 13.31, y: -2.09), controlPoint1: NSPoint(x: 11.62, y: -2.85), controlPoint2: NSPoint(x: 12.38, y: -2.09)) + bezier3Path.close() + bezier3Path.move(to: NSPoint(x: 13.31, y: -2.41)) + bezier3Path.curve(to: NSPoint(x: 11.93, y: -3.78), controlPoint1: NSPoint(x: 12.55, y: -2.41), controlPoint2: NSPoint(x: 11.93, y: -3.02)) + bezier3Path.curve(to: NSPoint(x: 13.31, y: -5.16), controlPoint1: NSPoint(x: 11.93, y: -4.55), controlPoint2: NSPoint(x: 12.55, y: -5.16)) + bezier3Path.curve(to: NSPoint(x: 14.69, y: -3.78), controlPoint1: NSPoint(x: 14.07, y: -5.16), controlPoint2: NSPoint(x: 14.69, y: -4.55)) + bezier3Path.curve(to: NSPoint(x: 13.31, y: -2.41), controlPoint1: NSPoint(x: 14.69, y: -3.02), controlPoint2: NSPoint(x: 14.07, y: -2.41)) + bezier3Path.close() + setupKeyFill.setFill() + bezier3Path.fill() + + + + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawSetupKeySelected(frame: NSRect = NSRect(x: 0, y: 0, width: 230, height: 230)) { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Color Declarations + let setupKeyFill = NSColor(red: 0, green: 0, blue: 0, alpha: 1) + + //// Group + NSGraphicsContext.saveGraphicsState() + context.translateBy(x: frame.minX + 0.00000 * frame.width, y: frame.minY + 1.00000 * frame.height) + context.scaleBy(x: 9.2, y: 9.2) + + + + //// Bezier Drawing + let bezierPath = NSBezierPath() + bezierPath.move(to: NSPoint(x: 9.44, y: -11.47)) + bezierPath.curve(to: NSPoint(x: 8.84, y: -12.12), controlPoint1: NSPoint(x: 9.21, y: -11.66), controlPoint2: NSPoint(x: 9.01, y: -11.88)) + bezierPath.curve(to: NSPoint(x: 3.42, y: -22.51), controlPoint1: NSPoint(x: 7.04, y: -15.58), controlPoint2: NSPoint(x: 5.23, y: -19.05)) + bezierPath.curve(to: NSPoint(x: 1.88, y: -22.56), controlPoint1: NSPoint(x: 2.85, y: -22.46), controlPoint2: NSPoint(x: 2.32, y: -22.42)) + bezierPath.curve(to: NSPoint(x: 1.18, y: -20.01), controlPoint1: NSPoint(x: 1.3, y: -21.64), controlPoint2: NSPoint(x: 1.18, y: -20.81)) + bezierPath.curve(to: NSPoint(x: 1.83, y: -19.8), controlPoint1: NSPoint(x: 1.4, y: -19.94), controlPoint2: NSPoint(x: 1.62, y: -19.87)) + bezierPath.curve(to: NSPoint(x: 2.26, y: -19.1), controlPoint1: NSPoint(x: 2.03, y: -19.6), controlPoint2: NSPoint(x: 2.17, y: -19.37)) + bezierPath.curve(to: NSPoint(x: 2.18, y: -18.46), controlPoint1: NSPoint(x: 2.24, y: -18.89), controlPoint2: NSPoint(x: 2.21, y: -18.68)) + bezierPath.curve(to: NSPoint(x: 3.06, y: -18.08), controlPoint1: NSPoint(x: 2.48, y: -18.33), controlPoint2: NSPoint(x: 2.77, y: -18.21)) + bezierPath.curve(to: NSPoint(x: 3.43, y: -17.5), controlPoint1: NSPoint(x: 3.22, y: -17.97), controlPoint2: NSPoint(x: 3.33, y: -17.77)) + bezierPath.curve(to: NSPoint(x: 3.42, y: -16.8), controlPoint1: NSPoint(x: 3.42, y: -17.27), controlPoint2: NSPoint(x: 3.42, y: -17.03)) + bezierPath.curve(to: NSPoint(x: 4.25, y: -16.5), controlPoint1: NSPoint(x: 3.7, y: -16.7), controlPoint2: NSPoint(x: 3.97, y: -16.6)) + bezierPath.curve(to: NSPoint(x: 4.62, y: -15.81), controlPoint1: NSPoint(x: 4.43, y: -16.31), controlPoint2: NSPoint(x: 4.55, y: -16.08)) + bezierPath.curve(to: NSPoint(x: 4.58, y: -14.51), controlPoint1: NSPoint(x: 4.61, y: -15.37), controlPoint2: NSPoint(x: 4.04, y: -14.74)) + bezierPath.curve(to: NSPoint(x: 5.01, y: -13.79), controlPoint1: NSPoint(x: 4.73, y: -14.27), controlPoint2: NSPoint(x: 4.87, y: -14.03)) + bezierPath.curve(to: NSPoint(x: 4.74, y: -12.86), controlPoint1: NSPoint(x: 4.92, y: -13.48), controlPoint2: NSPoint(x: 4.7, y: -12.95)) + bezierPath.curve(to: NSPoint(x: 5.6, y: -11.21), controlPoint1: NSPoint(x: 4.97, y: -12.32), controlPoint2: NSPoint(x: 5.37, y: -11.86)) + bezierPath.curve(to: NSPoint(x: 4.91, y: -10.32), controlPoint1: NSPoint(x: 5.38, y: -10.7), controlPoint2: NSPoint(x: 4.83, y: -10.81)) + bezierPath.curve(to: NSPoint(x: 6.16, y: -8.86), controlPoint1: NSPoint(x: 4.89, y: -9.71), controlPoint2: NSPoint(x: 5.83, y: -9.72)) + bezierPath.curve(to: NSPoint(x: 5.77, y: -3.31), controlPoint1: NSPoint(x: 5.03, y: -7.29), controlPoint2: NSPoint(x: 4.82, y: -5.15)) + bezierPath.curve(to: NSPoint(x: 11.06, y: -0.48), controlPoint1: NSPoint(x: 6.82, y: -1.31), controlPoint2: NSPoint(x: 8.96, y: -0.25)) + bezierPath.curve(to: NSPoint(x: 12.05, y: -0.39), controlPoint1: NSPoint(x: 11.38, y: -0.42), controlPoint2: NSPoint(x: 11.71, y: -0.39)) + bezierPath.curve(to: NSPoint(x: 13.1, y: -0.5), controlPoint1: NSPoint(x: 12.41, y: -0.39), controlPoint2: NSPoint(x: 12.76, y: -0.42)) + bezierPath.curve(to: NSPoint(x: 18.55, y: -3.27), controlPoint1: NSPoint(x: 15.25, y: -0.18), controlPoint2: NSPoint(x: 17.47, y: -1.23)) + bezierPath.curve(to: NSPoint(x: 18.18, y: -8.85), controlPoint1: NSPoint(x: 19.53, y: -5.11), controlPoint2: NSPoint(x: 19.32, y: -7.27)) + bezierPath.curve(to: NSPoint(x: 18.27, y: -10.65), controlPoint1: NSPoint(x: 18, y: -9.48), controlPoint2: NSPoint(x: 18.05, y: -10.09)) + bezierPath.curve(to: NSPoint(x: 23.76, y: -21.01), controlPoint1: NSPoint(x: 20.1, y: -14.1), controlPoint2: NSPoint(x: 21.93, y: -17.56)) + bezierPath.curve(to: NSPoint(x: 22.92, y: -22.31), controlPoint1: NSPoint(x: 23.39, y: -21.45), controlPoint2: NSPoint(x: 23.06, y: -21.87)) + bezierPath.curve(to: NSPoint(x: 20.42, y: -21.43), controlPoint1: NSPoint(x: 21.83, y: -22.26), controlPoint2: NSPoint(x: 21.08, y: -21.89)) + bezierPath.curve(to: NSPoint(x: 20.62, y: -20.78), controlPoint1: NSPoint(x: 20.49, y: -21.22), controlPoint2: NSPoint(x: 20.56, y: -21)) + bezierPath.curve(to: NSPoint(x: 20.29, y: -20.03), controlPoint1: NSPoint(x: 20.57, y: -20.5), controlPoint2: NSPoint(x: 20.46, y: -20.26)) + bezierPath.curve(to: NSPoint(x: 19.72, y: -19.73), controlPoint1: NSPoint(x: 20.1, y: -19.93), controlPoint2: NSPoint(x: 19.91, y: -19.83)) + bezierPath.curve(to: NSPoint(x: 19.9, y: -18.79), controlPoint1: NSPoint(x: 19.78, y: -19.42), controlPoint2: NSPoint(x: 19.84, y: -19.1)) + bezierPath.curve(to: NSPoint(x: 19.63, y: -18.16), controlPoint1: NSPoint(x: 19.9, y: -18.6), controlPoint2: NSPoint(x: 19.8, y: -18.39)) + bezierPath.curve(to: NSPoint(x: 19.06, y: -17.77), controlPoint1: NSPoint(x: 19.44, y: -18.03), controlPoint2: NSPoint(x: 19.25, y: -17.9)) + bezierPath.curve(to: NSPoint(x: 19.27, y: -16.92), controlPoint1: NSPoint(x: 19.13, y: -17.49), controlPoint2: NSPoint(x: 19.2, y: -17.2)) + bezierPath.curve(to: NSPoint(x: 18.92, y: -16.22), controlPoint1: NSPoint(x: 19.22, y: -16.66), controlPoint2: NSPoint(x: 19.09, y: -16.43)) + bezierPath.curve(to: NSPoint(x: 17.82, y: -15.51), controlPoint1: NSPoint(x: 18.55, y: -15.98), controlPoint2: NSPoint(x: 17.7, y: -16.09)) + bezierPath.curve(to: NSPoint(x: 17.47, y: -14.75), controlPoint1: NSPoint(x: 17.71, y: -15.26), controlPoint2: NSPoint(x: 17.59, y: -15.01)) + bezierPath.curve(to: NSPoint(x: 16.56, y: -14.45), controlPoint1: NSPoint(x: 17.17, y: -14.65), controlPoint2: NSPoint(x: 16.61, y: -14.53)) + bezierPath.curve(to: NSPoint(x: 15.69, y: -12.8), controlPoint1: NSPoint(x: 16.24, y: -13.95), controlPoint2: NSPoint(x: 16.09, y: -13.37)) + bezierPath.curve(to: NSPoint(x: 14.56, y: -12.87), controlPoint1: NSPoint(x: 15.14, y: -12.7), controlPoint2: NSPoint(x: 14.91, y: -13.21)) + bezierPath.curve(to: NSPoint(x: 14.06, y: -11.01), controlPoint1: NSPoint(x: 14.04, y: -12.54), controlPoint2: NSPoint(x: 14.59, y: -11.77)) + bezierPath.curve(to: NSPoint(x: 13.93, y: -11.01), controlPoint1: NSPoint(x: 14.02, y: -11.01), controlPoint2: NSPoint(x: 13.98, y: -11.01)) + bezierPath.curve(to: NSPoint(x: 13.6, y: -12.08), controlPoint1: NSPoint(x: 13.75, y: -11.34), controlPoint2: NSPoint(x: 13.64, y: -11.7)) + bezierPath.curve(to: NSPoint(x: 13.6, y: -23.8), controlPoint1: NSPoint(x: 13.6, y: -15.98), controlPoint2: NSPoint(x: 13.6, y: -19.89)) + bezierPath.curve(to: NSPoint(x: 12.25, y: -24.55), controlPoint1: NSPoint(x: 13.07, y: -24.02), controlPoint2: NSPoint(x: 12.57, y: -24.23)) + bezierPath.curve(to: NSPoint(x: 10.45, y: -22.61), controlPoint1: NSPoint(x: 11.31, y: -24), controlPoint2: NSPoint(x: 10.82, y: -23.32)) + bezierPath.curve(to: NSPoint(x: 10.93, y: -22.13), controlPoint1: NSPoint(x: 10.61, y: -22.45), controlPoint2: NSPoint(x: 10.77, y: -22.29)) + bezierPath.curve(to: NSPoint(x: 10.99, y: -21.31), controlPoint1: NSPoint(x: 11.02, y: -21.86), controlPoint2: NSPoint(x: 11.03, y: -21.59)) + bezierPath.curve(to: NSPoint(x: 10.62, y: -20.78), controlPoint1: NSPoint(x: 10.87, y: -21.13), controlPoint2: NSPoint(x: 10.75, y: -20.96)) + bezierPath.curve(to: NSPoint(x: 11.23, y: -20.03), controlPoint1: NSPoint(x: 10.82, y: -20.53), controlPoint2: NSPoint(x: 11.03, y: -20.28)) + bezierPath.curve(to: NSPoint(x: 11.28, y: -19.35), controlPoint1: NSPoint(x: 11.31, y: -19.87), controlPoint2: NSPoint(x: 11.32, y: -19.63)) + bezierPath.curve(to: NSPoint(x: 10.95, y: -18.74), controlPoint1: NSPoint(x: 11.17, y: -19.15), controlPoint2: NSPoint(x: 11.06, y: -18.94)) + bezierPath.curve(to: NSPoint(x: 11.55, y: -18.09), controlPoint1: NSPoint(x: 11.15, y: -18.52), controlPoint2: NSPoint(x: 11.35, y: -18.3)) + bezierPath.curve(to: NSPoint(x: 11.56, y: -17.3), controlPoint1: NSPoint(x: 11.62, y: -17.83), controlPoint2: NSPoint(x: 11.62, y: -17.57)) + bezierPath.curve(to: NSPoint(x: 10.92, y: -16.16), controlPoint1: NSPoint(x: 11.35, y: -16.92), controlPoint2: NSPoint(x: 10.55, y: -16.62)) + bezierPath.curve(to: NSPoint(x: 10.97, y: -15.33), controlPoint1: NSPoint(x: 10.94, y: -15.88), controlPoint2: NSPoint(x: 10.95, y: -15.61)) + bezierPath.curve(to: NSPoint(x: 10.3, y: -14.63), controlPoint1: NSPoint(x: 10.75, y: -15.1), controlPoint2: NSPoint(x: 10.31, y: -14.73)) + bezierPath.curve(to: NSPoint(x: 10.3, y: -12.77), controlPoint1: NSPoint(x: 10.26, y: -14.04), controlPoint2: NSPoint(x: 10.4, y: -13.46)) + bezierPath.curve(to: NSPoint(x: 9.27, y: -12.3), controlPoint1: NSPoint(x: 9.87, y: -12.42), controlPoint2: NSPoint(x: 9.43, y: -12.77)) + bezierPath.curve(to: NSPoint(x: 9.44, y: -11.47), controlPoint1: NSPoint(x: 9.12, y: -12.02), controlPoint2: NSPoint(x: 9.27, y: -11.77)) + bezierPath.close() + bezierPath.move(to: NSPoint(x: 13.31, y: -2.25)) + bezierPath.curve(to: NSPoint(x: 14.85, y: -3.78), controlPoint1: NSPoint(x: 14.16, y: -2.25), controlPoint2: NSPoint(x: 14.85, y: -2.94)) + bezierPath.curve(to: NSPoint(x: 13.31, y: -5.32), controlPoint1: NSPoint(x: 14.85, y: -4.63), controlPoint2: NSPoint(x: 14.16, y: -5.32)) + bezierPath.curve(to: NSPoint(x: 11.78, y: -3.78), controlPoint1: NSPoint(x: 12.46, y: -5.32), controlPoint2: NSPoint(x: 11.78, y: -4.63)) + bezierPath.curve(to: NSPoint(x: 13.31, y: -2.25), controlPoint1: NSPoint(x: 11.78, y: -2.94), controlPoint2: NSPoint(x: 12.46, y: -2.25)) + bezierPath.close() + bezierPath.windingRule = .evenOdd + setupKeyFill.setFill() + bezierPath.fill() + + + //// Bezier 2 Drawing + let bezier2Path = NSBezierPath() + bezier2Path.move(to: NSPoint(x: 9.5, y: -11.55)) + bezier2Path.curve(to: NSPoint(x: 8.93, y: -12.18), controlPoint1: NSPoint(x: 9.28, y: -11.73), controlPoint2: NSPoint(x: 9.09, y: -11.94)) + bezier2Path.curve(to: NSPoint(x: 3.52, y: -22.56), controlPoint1: NSPoint(x: 6.22, y: -17.36), controlPoint2: NSPoint(x: 6.22, y: -17.36)) + bezier2Path.line(to: NSPoint(x: 3.42, y: -22.62)) + bezier2Path.curve(to: NSPoint(x: 1.91, y: -22.65), controlPoint1: NSPoint(x: 2.59, y: -22.55), controlPoint2: NSPoint(x: 2.26, y: -22.55)) + bezier2Path.line(to: NSPoint(x: 1.79, y: -22.61)) + bezier2Path.curve(to: NSPoint(x: 1.08, y: -20.01), controlPoint1: NSPoint(x: 1.27, y: -21.77), controlPoint2: NSPoint(x: 1.07, y: -20.98)) + bezier2Path.line(to: NSPoint(x: 1.15, y: -19.91)) + bezier2Path.curve(to: NSPoint(x: 1.48, y: -19.8), controlPoint1: NSPoint(x: 1.31, y: -19.86), controlPoint2: NSPoint(x: 1.31, y: -19.86)) + bezier2Path.curve(to: NSPoint(x: 1.8, y: -19.7), controlPoint1: NSPoint(x: 1.64, y: -19.75), controlPoint2: NSPoint(x: 1.64, y: -19.75)) + bezier2Path.curve(to: NSPoint(x: 2.17, y: -19.07), controlPoint1: NSPoint(x: 1.94, y: -19.55), controlPoint2: NSPoint(x: 2.08, y: -19.33)) + bezier2Path.curve(to: NSPoint(x: 2.14, y: -18.96), controlPoint1: NSPoint(x: 2.15, y: -19.04), controlPoint2: NSPoint(x: 2.15, y: -19.04)) + bezier2Path.curve(to: NSPoint(x: 2.12, y: -18.78), controlPoint1: NSPoint(x: 2.13, y: -18.87), controlPoint2: NSPoint(x: 2.13, y: -18.87)) + bezier2Path.curve(to: NSPoint(x: 2.08, y: -18.48), controlPoint1: NSPoint(x: 2.1, y: -18.63), controlPoint2: NSPoint(x: 2.1, y: -18.63)) + bezier2Path.line(to: NSPoint(x: 2.14, y: -18.37)) + bezier2Path.line(to: NSPoint(x: 2.14, y: -18.37)) + bezier2Path.curve(to: NSPoint(x: 3.02, y: -17.98), controlPoint1: NSPoint(x: 2.58, y: -18.17), controlPoint2: NSPoint(x: 2.58, y: -18.17)) + bezier2Path.curve(to: NSPoint(x: 3.33, y: -17.46), controlPoint1: NSPoint(x: 3.13, y: -17.91), controlPoint2: NSPoint(x: 3.24, y: -17.73)) + bezier2Path.line(to: NSPoint(x: 3.32, y: -17.5)) + bezier2Path.curve(to: NSPoint(x: 3.32, y: -16.94), controlPoint1: NSPoint(x: 3.32, y: -17.22), controlPoint2: NSPoint(x: 3.32, y: -17.22)) + bezier2Path.curve(to: NSPoint(x: 3.32, y: -16.8), controlPoint1: NSPoint(x: 3.32, y: -16.87), controlPoint2: NSPoint(x: 3.32, y: -16.87)) + bezier2Path.line(to: NSPoint(x: 3.39, y: -16.71)) + bezier2Path.curve(to: NSPoint(x: 4.21, y: -16.4), controlPoint1: NSPoint(x: 3.8, y: -16.55), controlPoint2: NSPoint(x: 3.8, y: -16.55)) + bezier2Path.curve(to: NSPoint(x: 4.52, y: -15.78), controlPoint1: NSPoint(x: 4.33, y: -16.26), controlPoint2: NSPoint(x: 4.45, y: -16.05)) + bezier2Path.curve(to: NSPoint(x: 4.39, y: -15.37), controlPoint1: NSPoint(x: 4.52, y: -15.71), controlPoint2: NSPoint(x: 4.48, y: -15.61)) + bezier2Path.line(to: NSPoint(x: 4.39, y: -15.37)) + bezier2Path.curve(to: NSPoint(x: 4.54, y: -14.41), controlPoint1: NSPoint(x: 4.17, y: -14.81), controlPoint2: NSPoint(x: 4.16, y: -14.57)) + bezier2Path.line(to: NSPoint(x: 4.49, y: -14.45)) + bezier2Path.curve(to: NSPoint(x: 4.6, y: -14.27), controlPoint1: NSPoint(x: 4.55, y: -14.36), controlPoint2: NSPoint(x: 4.55, y: -14.36)) + bezier2Path.curve(to: NSPoint(x: 4.62, y: -14.25), controlPoint1: NSPoint(x: 4.61, y: -14.26), controlPoint2: NSPoint(x: 4.61, y: -14.26)) + bezier2Path.curve(to: NSPoint(x: 4.92, y: -13.74), controlPoint1: NSPoint(x: 4.77, y: -13.99), controlPoint2: NSPoint(x: 4.77, y: -13.99)) + bezier2Path.line(to: NSPoint(x: 4.91, y: -13.82)) + bezier2Path.curve(to: NSPoint(x: 4.79, y: -13.47), controlPoint1: NSPoint(x: 4.89, y: -13.74), controlPoint2: NSPoint(x: 4.86, y: -13.66)) + bezier2Path.line(to: NSPoint(x: 4.79, y: -13.47)) + bezier2Path.curve(to: NSPoint(x: 4.65, y: -12.82), controlPoint1: NSPoint(x: 4.63, y: -13), controlPoint2: NSPoint(x: 4.6, y: -12.91)) + bezier2Path.curve(to: NSPoint(x: 4.9, y: -12.32), controlPoint1: NSPoint(x: 4.71, y: -12.67), controlPoint2: NSPoint(x: 4.79, y: -12.51)) + bezier2Path.curve(to: NSPoint(x: 5.09, y: -12.01), controlPoint1: NSPoint(x: 4.95, y: -12.25), controlPoint2: NSPoint(x: 4.99, y: -12.17)) + bezier2Path.curve(to: NSPoint(x: 5.5, y: -11.18), controlPoint1: NSPoint(x: 5.29, y: -11.67), controlPoint2: NSPoint(x: 5.41, y: -11.45)) + bezier2Path.line(to: NSPoint(x: 5.51, y: -11.25)) + bezier2Path.curve(to: NSPoint(x: 5.24, y: -10.94), controlPoint1: NSPoint(x: 5.45, y: -11.12), controlPoint2: NSPoint(x: 5.36, y: -11.03)) + bezier2Path.curve(to: NSPoint(x: 5.07, y: -10.84), controlPoint1: NSPoint(x: 5.2, y: -10.91), controlPoint2: NSPoint(x: 5.04, y: -10.82)) + bezier2Path.curve(to: NSPoint(x: 4.8, y: -10.3), controlPoint1: NSPoint(x: 4.85, y: -10.69), controlPoint2: NSPoint(x: 4.76, y: -10.56)) + bezier2Path.curve(to: NSPoint(x: 5.21, y: -9.68), controlPoint1: NSPoint(x: 4.79, y: -10.05), controlPoint2: NSPoint(x: 4.92, y: -9.89)) + bezier2Path.curve(to: NSPoint(x: 5.53, y: -9.47), controlPoint1: NSPoint(x: 5.23, y: -9.67), controlPoint2: NSPoint(x: 5.46, y: -9.52)) + bezier2Path.curve(to: NSPoint(x: 6.06, y: -8.82), controlPoint1: NSPoint(x: 5.79, y: -9.28), controlPoint2: NSPoint(x: 5.96, y: -9.09)) + bezier2Path.line(to: NSPoint(x: 6.07, y: -8.92)) + bezier2Path.curve(to: NSPoint(x: 5.68, y: -3.26), controlPoint1: NSPoint(x: 4.9, y: -7.28), controlPoint2: NSPoint(x: 4.73, y: -5.1)) + bezier2Path.curve(to: NSPoint(x: 11.07, y: -0.38), controlPoint1: NSPoint(x: 6.73, y: -1.26), controlPoint2: NSPoint(x: 8.88, y: -0.13)) + bezier2Path.curve(to: NSPoint(x: 12.05, y: -0.28), controlPoint1: NSPoint(x: 11.37, y: -0.32), controlPoint2: NSPoint(x: 11.71, y: -0.28)) + bezier2Path.curve(to: NSPoint(x: 13.13, y: -0.39), controlPoint1: NSPoint(x: 12.42, y: -0.28), controlPoint2: NSPoint(x: 12.77, y: -0.32)) + bezier2Path.curve(to: NSPoint(x: 18.64, y: -3.22), controlPoint1: NSPoint(x: 15.32, y: -0.06), controlPoint2: NSPoint(x: 17.56, y: -1.18)) + bezier2Path.curve(to: NSPoint(x: 18.27, y: -8.91), controlPoint1: NSPoint(x: 19.62, y: -5.06), controlPoint2: NSPoint(x: 19.45, y: -7.26)) + bezier2Path.curve(to: NSPoint(x: 18.37, y: -10.61), controlPoint1: NSPoint(x: 18.11, y: -9.46), controlPoint2: NSPoint(x: 18.15, y: -10.05)) + bezier2Path.curve(to: NSPoint(x: 21.11, y: -15.78), controlPoint1: NSPoint(x: 19.74, y: -13.19), controlPoint2: NSPoint(x: 19.74, y: -13.19)) + bezier2Path.curve(to: NSPoint(x: 21.5, y: -16.52), controlPoint1: NSPoint(x: 21.3, y: -16.15), controlPoint2: NSPoint(x: 21.3, y: -16.15)) + bezier2Path.curve(to: NSPoint(x: 23.85, y: -20.96), controlPoint1: NSPoint(x: 22.68, y: -18.74), controlPoint2: NSPoint(x: 22.68, y: -18.74)) + bezier2Path.line(to: NSPoint(x: 23.84, y: -21.07)) + bezier2Path.curve(to: NSPoint(x: 23.3, y: -21.78), controlPoint1: NSPoint(x: 23.54, y: -21.43), controlPoint2: NSPoint(x: 23.43, y: -21.58)) + bezier2Path.curve(to: NSPoint(x: 23.02, y: -22.34), controlPoint1: NSPoint(x: 23.17, y: -21.98), controlPoint2: NSPoint(x: 23.07, y: -22.16)) + bezier2Path.line(to: NSPoint(x: 22.91, y: -22.41)) + bezier2Path.curve(to: NSPoint(x: 20.36, y: -21.52), controlPoint1: NSPoint(x: 21.93, y: -22.36), controlPoint2: NSPoint(x: 21.16, y: -22.07)) + bezier2Path.line(to: NSPoint(x: 20.32, y: -21.4)) + bezier2Path.curve(to: NSPoint(x: 20.39, y: -21.19), controlPoint1: NSPoint(x: 20.36, y: -21.29), controlPoint2: NSPoint(x: 20.36, y: -21.29)) + bezier2Path.curve(to: NSPoint(x: 20.43, y: -21.05), controlPoint1: NSPoint(x: 20.41, y: -21.12), controlPoint2: NSPoint(x: 20.41, y: -21.12)) + bezier2Path.curve(to: NSPoint(x: 20.52, y: -20.75), controlPoint1: NSPoint(x: 20.48, y: -20.9), controlPoint2: NSPoint(x: 20.48, y: -20.9)) + bezier2Path.curve(to: NSPoint(x: 20.21, y: -20.09), controlPoint1: NSPoint(x: 20.48, y: -20.54), controlPoint2: NSPoint(x: 20.37, y: -20.31)) + bezier2Path.curve(to: NSPoint(x: 19.96, y: -19.97), controlPoint1: NSPoint(x: 20.1, y: -20.05), controlPoint2: NSPoint(x: 20.1, y: -20.05)) + bezier2Path.curve(to: NSPoint(x: 19.93, y: -19.96), controlPoint1: NSPoint(x: 19.95, y: -19.97), controlPoint2: NSPoint(x: 19.95, y: -19.97)) + bezier2Path.curve(to: NSPoint(x: 19.67, y: -19.83), controlPoint1: NSPoint(x: 19.8, y: -19.89), controlPoint2: NSPoint(x: 19.8, y: -19.89)) + bezier2Path.line(to: NSPoint(x: 19.62, y: -19.71)) + bezier2Path.curve(to: NSPoint(x: 19.71, y: -19.24), controlPoint1: NSPoint(x: 19.66, y: -19.48), controlPoint2: NSPoint(x: 19.66, y: -19.48)) + bezier2Path.curve(to: NSPoint(x: 19.76, y: -18.96), controlPoint1: NSPoint(x: 19.74, y: -19.1), controlPoint2: NSPoint(x: 19.74, y: -19.1)) + bezier2Path.curve(to: NSPoint(x: 19.8, y: -18.77), controlPoint1: NSPoint(x: 19.78, y: -18.86), controlPoint2: NSPoint(x: 19.78, y: -18.86)) + bezier2Path.curve(to: NSPoint(x: 19.55, y: -18.22), controlPoint1: NSPoint(x: 19.8, y: -18.64), controlPoint2: NSPoint(x: 19.71, y: -18.45)) + bezier2Path.curve(to: NSPoint(x: 19.28, y: -18.05), controlPoint1: NSPoint(x: 19.43, y: -18.15), controlPoint2: NSPoint(x: 19.43, y: -18.15)) + bezier2Path.curve(to: NSPoint(x: 19, y: -17.86), controlPoint1: NSPoint(x: 19.14, y: -17.95), controlPoint2: NSPoint(x: 19.14, y: -17.95)) + bezier2Path.line(to: NSPoint(x: 18.95, y: -17.75)) + bezier2Path.curve(to: NSPoint(x: 19.04, y: -17.41), controlPoint1: NSPoint(x: 19, y: -17.58), controlPoint2: NSPoint(x: 19, y: -17.58)) + bezier2Path.curve(to: NSPoint(x: 19.06, y: -17.32), controlPoint1: NSPoint(x: 19.05, y: -17.36), controlPoint2: NSPoint(x: 19.05, y: -17.36)) + bezier2Path.curve(to: NSPoint(x: 19.17, y: -16.89), controlPoint1: NSPoint(x: 19.12, y: -17.11), controlPoint2: NSPoint(x: 19.12, y: -17.11)) + bezier2Path.curve(to: NSPoint(x: 18.84, y: -16.28), controlPoint1: NSPoint(x: 19.13, y: -16.71), controlPoint2: NSPoint(x: 19.01, y: -16.5)) + bezier2Path.curve(to: NSPoint(x: 18.42, y: -16.16), controlPoint1: NSPoint(x: 18.77, y: -16.25), controlPoint2: NSPoint(x: 18.67, y: -16.22)) + bezier2Path.line(to: NSPoint(x: 18.42, y: -16.16)) + bezier2Path.curve(to: NSPoint(x: 17.72, y: -15.49), controlPoint1: NSPoint(x: 17.84, y: -16.03), controlPoint2: NSPoint(x: 17.64, y: -15.89)) + bezier2Path.line(to: NSPoint(x: 17.73, y: -15.55)) + bezier2Path.curve(to: NSPoint(x: 17.38, y: -14.8), controlPoint1: NSPoint(x: 17.55, y: -15.18), controlPoint2: NSPoint(x: 17.55, y: -15.18)) + bezier2Path.line(to: NSPoint(x: 17.44, y: -14.85)) + bezier2Path.curve(to: NSPoint(x: 17.08, y: -14.75), controlPoint1: NSPoint(x: 17.36, y: -14.83), controlPoint2: NSPoint(x: 17.28, y: -14.8)) + bezier2Path.line(to: NSPoint(x: 17.08, y: -14.75)) + bezier2Path.curve(to: NSPoint(x: 16.47, y: -14.5), controlPoint1: NSPoint(x: 16.61, y: -14.62), controlPoint2: NSPoint(x: 16.52, y: -14.59)) + bezier2Path.curve(to: NSPoint(x: 16.14, y: -13.86), controlPoint1: NSPoint(x: 16.36, y: -14.33), controlPoint2: NSPoint(x: 16.27, y: -14.16)) + bezier2Path.curve(to: NSPoint(x: 15.83, y: -13.23), controlPoint1: NSPoint(x: 15.96, y: -13.46), controlPoint2: NSPoint(x: 15.93, y: -13.4)) + bezier2Path.curve(to: NSPoint(x: 15.6, y: -12.87), controlPoint1: NSPoint(x: 15.76, y: -13.1), controlPoint2: NSPoint(x: 15.69, y: -12.98)) + bezier2Path.line(to: NSPoint(x: 15.67, y: -12.91)) + bezier2Path.curve(to: NSPoint(x: 15.17, y: -12.99), controlPoint1: NSPoint(x: 15.5, y: -12.87), controlPoint2: NSPoint(x: 15.37, y: -12.9)) + bezier2Path.curve(to: NSPoint(x: 14.85, y: -13.09), controlPoint1: NSPoint(x: 14.96, y: -13.08), controlPoint2: NSPoint(x: 14.94, y: -13.08)) + bezier2Path.curve(to: NSPoint(x: 14.49, y: -12.95), controlPoint1: NSPoint(x: 14.72, y: -13.1), controlPoint2: NSPoint(x: 14.6, y: -13.06)) + bezier2Path.curve(to: NSPoint(x: 14.21, y: -12.1), controlPoint1: NSPoint(x: 14.24, y: -12.79), controlPoint2: NSPoint(x: 14.2, y: -12.6)) + bezier2Path.curve(to: NSPoint(x: 13.99, y: -11.08), controlPoint1: NSPoint(x: 14.22, y: -11.61), controlPoint2: NSPoint(x: 14.18, y: -11.37)) + bezier2Path.curve(to: NSPoint(x: 13.98, y: -11.07), controlPoint1: NSPoint(x: 13.98, y: -11.08), controlPoint2: NSPoint(x: 13.98, y: -11.08)) + bezier2Path.line(to: NSPoint(x: 14.06, y: -11.01)) + bezier2Path.line(to: NSPoint(x: 14.06, y: -11.11)) + bezier2Path.curve(to: NSPoint(x: 13.93, y: -11.12), controlPoint1: NSPoint(x: 14, y: -11.12), controlPoint2: NSPoint(x: 14, y: -11.12)) + bezier2Path.line(to: NSPoint(x: 13.93, y: -11.01)) + bezier2Path.line(to: NSPoint(x: 14.03, y: -11.06)) + bezier2Path.curve(to: NSPoint(x: 13.7, y: -12.09), controlPoint1: NSPoint(x: 13.85, y: -11.37), controlPoint2: NSPoint(x: 13.74, y: -11.72)) + bezier2Path.curve(to: NSPoint(x: 13.7, y: -17.94), controlPoint1: NSPoint(x: 13.7, y: -15.01), controlPoint2: NSPoint(x: 13.7, y: -15.01)) + bezier2Path.curve(to: NSPoint(x: 13.7, y: -23.8), controlPoint1: NSPoint(x: 13.7, y: -20.87), controlPoint2: NSPoint(x: 13.7, y: -20.87)) + bezier2Path.line(to: NSPoint(x: 13.64, y: -23.89)) + bezier2Path.curve(to: NSPoint(x: 13.45, y: -23.97), controlPoint1: NSPoint(x: 13.54, y: -23.93), controlPoint2: NSPoint(x: 13.54, y: -23.93)) + bezier2Path.curve(to: NSPoint(x: 12.32, y: -24.62), controlPoint1: NSPoint(x: 12.86, y: -24.22), controlPoint2: NSPoint(x: 12.55, y: -24.39)) + bezier2Path.line(to: NSPoint(x: 12.19, y: -24.64)) + bezier2Path.curve(to: NSPoint(x: 10.36, y: -22.66), controlPoint1: NSPoint(x: 11.34, y: -24.14), controlPoint2: NSPoint(x: 10.8, y: -23.52)) + bezier2Path.line(to: NSPoint(x: 10.37, y: -22.54)) + bezier2Path.curve(to: NSPoint(x: 10.54, y: -22.38), controlPoint1: NSPoint(x: 10.45, y: -22.46), controlPoint2: NSPoint(x: 10.45, y: -22.46)) + bezier2Path.curve(to: NSPoint(x: 10.62, y: -22.3), controlPoint1: NSPoint(x: 10.58, y: -22.34), controlPoint2: NSPoint(x: 10.58, y: -22.34)) + bezier2Path.curve(to: NSPoint(x: 10.86, y: -22.05), controlPoint1: NSPoint(x: 10.74, y: -22.18), controlPoint2: NSPoint(x: 10.74, y: -22.18)) + bezier2Path.curve(to: NSPoint(x: 10.89, y: -21.33), controlPoint1: NSPoint(x: 10.91, y: -21.85), controlPoint2: NSPoint(x: 10.93, y: -21.59)) + bezier2Path.curve(to: NSPoint(x: 10.72, y: -21.1), controlPoint1: NSPoint(x: 10.81, y: -21.24), controlPoint2: NSPoint(x: 10.81, y: -21.24)) + bezier2Path.curve(to: NSPoint(x: 10.54, y: -20.84), controlPoint1: NSPoint(x: 10.63, y: -20.97), controlPoint2: NSPoint(x: 10.63, y: -20.97)) + bezier2Path.line(to: NSPoint(x: 10.54, y: -20.71)) + bezier2Path.curve(to: NSPoint(x: 10.78, y: -20.42), controlPoint1: NSPoint(x: 10.66, y: -20.57), controlPoint2: NSPoint(x: 10.66, y: -20.57)) + bezier2Path.curve(to: NSPoint(x: 10.84, y: -20.34), controlPoint1: NSPoint(x: 10.81, y: -20.38), controlPoint2: NSPoint(x: 10.81, y: -20.38)) + bezier2Path.curve(to: NSPoint(x: 11.15, y: -19.97), controlPoint1: NSPoint(x: 11, y: -20.15), controlPoint2: NSPoint(x: 11, y: -20.15)) + bezier2Path.curve(to: NSPoint(x: 11.18, y: -19.37), controlPoint1: NSPoint(x: 11.21, y: -19.85), controlPoint2: NSPoint(x: 11.22, y: -19.64)) + bezier2Path.line(to: NSPoint(x: 11.19, y: -19.4)) + bezier2Path.curve(to: NSPoint(x: 10.86, y: -18.79), controlPoint1: NSPoint(x: 11.03, y: -19.09), controlPoint2: NSPoint(x: 11.03, y: -19.09)) + bezier2Path.line(to: NSPoint(x: 10.86, y: -18.79)) + bezier2Path.line(to: NSPoint(x: 10.88, y: -18.67)) + bezier2Path.line(to: NSPoint(x: 10.88, y: -18.67)) + bezier2Path.curve(to: NSPoint(x: 11.47, y: -18.02), controlPoint1: NSPoint(x: 11.17, y: -18.34), controlPoint2: NSPoint(x: 11.17, y: -18.34)) + bezier2Path.line(to: NSPoint(x: 11.47, y: -18.02)) + bezier2Path.curve(to: NSPoint(x: 11.46, y: -17.32), controlPoint1: NSPoint(x: 11.51, y: -17.83), controlPoint2: NSPoint(x: 11.51, y: -17.59)) + bezier2Path.curve(to: NSPoint(x: 11.15, y: -17.01), controlPoint1: NSPoint(x: 11.42, y: -17.26), controlPoint2: NSPoint(x: 11.34, y: -17.18)) + bezier2Path.line(to: NSPoint(x: 11.15, y: -17.01)) + bezier2Path.curve(to: NSPoint(x: 10.84, y: -16.1), controlPoint1: NSPoint(x: 10.7, y: -16.63), controlPoint2: NSPoint(x: 10.58, y: -16.41)) + bezier2Path.line(to: NSPoint(x: 10.82, y: -16.16)) + bezier2Path.curve(to: NSPoint(x: 10.85, y: -15.56), controlPoint1: NSPoint(x: 10.84, y: -15.86), controlPoint2: NSPoint(x: 10.84, y: -15.86)) + bezier2Path.curve(to: NSPoint(x: 10.87, y: -15.32), controlPoint1: NSPoint(x: 10.86, y: -15.44), controlPoint2: NSPoint(x: 10.86, y: -15.44)) + bezier2Path.line(to: NSPoint(x: 10.89, y: -15.4)) + bezier2Path.curve(to: NSPoint(x: 10.63, y: -15.14), controlPoint1: NSPoint(x: 10.84, y: -15.34), controlPoint2: NSPoint(x: 10.77, y: -15.28)) + bezier2Path.line(to: NSPoint(x: 10.63, y: -15.14)) + bezier2Path.curve(to: NSPoint(x: 10.2, y: -14.64), controlPoint1: NSPoint(x: 10.27, y: -14.81), controlPoint2: NSPoint(x: 10.2, y: -14.74)) + bezier2Path.curve(to: NSPoint(x: 10.21, y: -13.83), controlPoint1: NSPoint(x: 10.18, y: -14.42), controlPoint2: NSPoint(x: 10.19, y: -14.25)) + bezier2Path.curve(to: NSPoint(x: 10.2, y: -12.79), controlPoint1: NSPoint(x: 10.24, y: -13.33), controlPoint2: NSPoint(x: 10.24, y: -13.09)) + bezier2Path.line(to: NSPoint(x: 10.24, y: -12.85)) + bezier2Path.curve(to: NSPoint(x: 10.23, y: -12.85), controlPoint1: NSPoint(x: 10.24, y: -12.85), controlPoint2: NSPoint(x: 10.24, y: -12.85)) + bezier2Path.curve(to: NSPoint(x: 9.72, y: -12.69), controlPoint1: NSPoint(x: 10.09, y: -12.73), controlPoint2: NSPoint(x: 9.98, y: -12.7)) + bezier2Path.curve(to: NSPoint(x: 9.18, y: -12.33), controlPoint1: NSPoint(x: 9.4, y: -12.66), controlPoint2: NSPoint(x: 9.26, y: -12.6)) + bezier2Path.curve(to: NSPoint(x: 9.11, y: -11.99), controlPoint1: NSPoint(x: 9.12, y: -12.24), controlPoint2: NSPoint(x: 9.1, y: -12.12)) + bezier2Path.curve(to: NSPoint(x: 9.35, y: -11.42), controlPoint1: NSPoint(x: 9.13, y: -11.83), controlPoint2: NSPoint(x: 9.17, y: -11.75)) + bezier2Path.line(to: NSPoint(x: 9.5, y: -11.55)) + bezier2Path.close() + bezier2Path.move(to: NSPoint(x: 9.37, y: -11.39)) + bezier2Path.line(to: NSPoint(x: 9.53, y: -11.52)) + bezier2Path.curve(to: NSPoint(x: 9.32, y: -12.01), controlPoint1: NSPoint(x: 9.37, y: -11.82), controlPoint2: NSPoint(x: 9.33, y: -11.89)) + bezier2Path.curve(to: NSPoint(x: 9.37, y: -12.25), controlPoint1: NSPoint(x: 9.31, y: -12.1), controlPoint2: NSPoint(x: 9.32, y: -12.17)) + bezier2Path.curve(to: NSPoint(x: 9.73, y: -12.48), controlPoint1: NSPoint(x: 9.43, y: -12.43), controlPoint2: NSPoint(x: 9.49, y: -12.46)) + bezier2Path.curve(to: NSPoint(x: 10.36, y: -12.69), controlPoint1: NSPoint(x: 10.03, y: -12.5), controlPoint2: NSPoint(x: 10.18, y: -12.54)) + bezier2Path.curve(to: NSPoint(x: 10.37, y: -12.69), controlPoint1: NSPoint(x: 10.37, y: -12.69), controlPoint2: NSPoint(x: 10.37, y: -12.69)) + bezier2Path.line(to: NSPoint(x: 10.41, y: -12.76)) + bezier2Path.curve(to: NSPoint(x: 10.42, y: -13.84), controlPoint1: NSPoint(x: 10.45, y: -13.08), controlPoint2: NSPoint(x: 10.45, y: -13.33)) + bezier2Path.curve(to: NSPoint(x: 10.41, y: -14.62), controlPoint1: NSPoint(x: 10.39, y: -14.25), controlPoint2: NSPoint(x: 10.39, y: -14.41)) + bezier2Path.curve(to: NSPoint(x: 10.77, y: -14.99), controlPoint1: NSPoint(x: 10.41, y: -14.64), controlPoint2: NSPoint(x: 10.54, y: -14.77)) + bezier2Path.line(to: NSPoint(x: 10.77, y: -14.99)) + bezier2Path.curve(to: NSPoint(x: 11.05, y: -15.26), controlPoint1: NSPoint(x: 10.92, y: -15.13), controlPoint2: NSPoint(x: 10.98, y: -15.19)) + bezier2Path.line(to: NSPoint(x: 11.07, y: -15.34)) + bezier2Path.curve(to: NSPoint(x: 11.06, y: -15.57), controlPoint1: NSPoint(x: 11.07, y: -15.45), controlPoint2: NSPoint(x: 11.07, y: -15.45)) + bezier2Path.curve(to: NSPoint(x: 11.03, y: -16.17), controlPoint1: NSPoint(x: 11.04, y: -15.87), controlPoint2: NSPoint(x: 11.04, y: -15.87)) + bezier2Path.line(to: NSPoint(x: 11, y: -16.23)) + bezier2Path.curve(to: NSPoint(x: 11.29, y: -16.86), controlPoint1: NSPoint(x: 10.85, y: -16.42), controlPoint2: NSPoint(x: 10.92, y: -16.54)) + bezier2Path.line(to: NSPoint(x: 11.29, y: -16.86)) + bezier2Path.curve(to: NSPoint(x: 11.65, y: -17.25), controlPoint1: NSPoint(x: 11.5, y: -17.04), controlPoint2: NSPoint(x: 11.58, y: -17.13)) + bezier2Path.curve(to: NSPoint(x: 11.64, y: -18.12), controlPoint1: NSPoint(x: 11.72, y: -17.58), controlPoint2: NSPoint(x: 11.72, y: -17.86)) + bezier2Path.line(to: NSPoint(x: 11.62, y: -18.16)) + bezier2Path.curve(to: NSPoint(x: 11.03, y: -18.81), controlPoint1: NSPoint(x: 11.33, y: -18.48), controlPoint2: NSPoint(x: 11.33, y: -18.48)) + bezier2Path.line(to: NSPoint(x: 11.03, y: -18.81)) + bezier2Path.line(to: NSPoint(x: 11.05, y: -18.69)) + bezier2Path.line(to: NSPoint(x: 11.05, y: -18.69)) + bezier2Path.curve(to: NSPoint(x: 11.37, y: -19.3), controlPoint1: NSPoint(x: 11.21, y: -18.99), controlPoint2: NSPoint(x: 11.21, y: -18.99)) + bezier2Path.line(to: NSPoint(x: 11.37, y: -19.3)) + bezier2Path.curve(to: NSPoint(x: 11.32, y: -20.08), controlPoint1: NSPoint(x: 11.43, y: -19.65), controlPoint2: NSPoint(x: 11.42, y: -19.9)) + bezier2Path.curve(to: NSPoint(x: 11.01, y: -20.47), controlPoint1: NSPoint(x: 11.16, y: -20.28), controlPoint2: NSPoint(x: 11.16, y: -20.28)) + bezier2Path.curve(to: NSPoint(x: 10.94, y: -20.55), controlPoint1: NSPoint(x: 10.98, y: -20.51), controlPoint2: NSPoint(x: 10.98, y: -20.51)) + bezier2Path.curve(to: NSPoint(x: 10.7, y: -20.85), controlPoint1: NSPoint(x: 10.82, y: -20.7), controlPoint2: NSPoint(x: 10.82, y: -20.7)) + bezier2Path.line(to: NSPoint(x: 10.71, y: -20.72)) + bezier2Path.curve(to: NSPoint(x: 10.89, y: -20.99), controlPoint1: NSPoint(x: 10.8, y: -20.85), controlPoint2: NSPoint(x: 10.8, y: -20.85)) + bezier2Path.curve(to: NSPoint(x: 11.08, y: -21.25), controlPoint1: NSPoint(x: 10.99, y: -21.12), controlPoint2: NSPoint(x: 10.99, y: -21.12)) + bezier2Path.curve(to: NSPoint(x: 11.03, y: -22.16), controlPoint1: NSPoint(x: 11.14, y: -21.59), controlPoint2: NSPoint(x: 11.12, y: -21.88)) + bezier2Path.curve(to: NSPoint(x: 10.76, y: -22.44), controlPoint1: NSPoint(x: 10.88, y: -22.32), controlPoint2: NSPoint(x: 10.88, y: -22.32)) + bezier2Path.curve(to: NSPoint(x: 10.68, y: -22.52), controlPoint1: NSPoint(x: 10.72, y: -22.48), controlPoint2: NSPoint(x: 10.72, y: -22.48)) + bezier2Path.curve(to: NSPoint(x: 10.52, y: -22.69), controlPoint1: NSPoint(x: 10.6, y: -22.6), controlPoint2: NSPoint(x: 10.6, y: -22.6)) + bezier2Path.line(to: NSPoint(x: 10.54, y: -22.56)) + bezier2Path.curve(to: NSPoint(x: 12.3, y: -24.46), controlPoint1: NSPoint(x: 10.97, y: -23.4), controlPoint2: NSPoint(x: 11.48, y: -23.98)) + bezier2Path.line(to: NSPoint(x: 12.17, y: -24.48)) + bezier2Path.curve(to: NSPoint(x: 13.37, y: -23.78), controlPoint1: NSPoint(x: 12.43, y: -24.22), controlPoint2: NSPoint(x: 12.76, y: -24.04)) + bezier2Path.curve(to: NSPoint(x: 13.56, y: -23.7), controlPoint1: NSPoint(x: 13.46, y: -23.74), controlPoint2: NSPoint(x: 13.46, y: -23.74)) + bezier2Path.line(to: NSPoint(x: 13.49, y: -23.8)) + bezier2Path.curve(to: NSPoint(x: 13.49, y: -17.94), controlPoint1: NSPoint(x: 13.49, y: -20.87), controlPoint2: NSPoint(x: 13.49, y: -20.87)) + bezier2Path.curve(to: NSPoint(x: 13.49, y: -12.08), controlPoint1: NSPoint(x: 13.49, y: -15.01), controlPoint2: NSPoint(x: 13.49, y: -15.01)) + bezier2Path.curve(to: NSPoint(x: 13.84, y: -10.96), controlPoint1: NSPoint(x: 13.54, y: -11.67), controlPoint2: NSPoint(x: 13.65, y: -11.3)) + bezier2Path.line(to: NSPoint(x: 13.93, y: -10.91)) + bezier2Path.curve(to: NSPoint(x: 14.06, y: -10.91), controlPoint1: NSPoint(x: 14, y: -10.91), controlPoint2: NSPoint(x: 14, y: -10.91)) + bezier2Path.line(to: NSPoint(x: 14.15, y: -10.95)) + bezier2Path.curve(to: NSPoint(x: 14.16, y: -10.97), controlPoint1: NSPoint(x: 14.15, y: -10.96), controlPoint2: NSPoint(x: 14.15, y: -10.96)) + bezier2Path.curve(to: NSPoint(x: 14.42, y: -12.11), controlPoint1: NSPoint(x: 14.38, y: -11.3), controlPoint2: NSPoint(x: 14.43, y: -11.58)) + bezier2Path.curve(to: NSPoint(x: 14.62, y: -12.79), controlPoint1: NSPoint(x: 14.41, y: -12.54), controlPoint2: NSPoint(x: 14.44, y: -12.67)) + bezier2Path.curve(to: NSPoint(x: 14.83, y: -12.89), controlPoint1: NSPoint(x: 14.7, y: -12.87), controlPoint2: NSPoint(x: 14.76, y: -12.89)) + bezier2Path.curve(to: NSPoint(x: 15.09, y: -12.79), controlPoint1: NSPoint(x: 14.89, y: -12.88), controlPoint2: NSPoint(x: 14.91, y: -12.87)) + bezier2Path.curve(to: NSPoint(x: 15.71, y: -12.7), controlPoint1: NSPoint(x: 15.32, y: -12.69), controlPoint2: NSPoint(x: 15.49, y: -12.66)) + bezier2Path.line(to: NSPoint(x: 15.77, y: -12.74)) + bezier2Path.curve(to: NSPoint(x: 16.01, y: -13.13), controlPoint1: NSPoint(x: 15.86, y: -12.86), controlPoint2: NSPoint(x: 15.94, y: -12.99)) + bezier2Path.curve(to: NSPoint(x: 16.33, y: -13.78), controlPoint1: NSPoint(x: 16.11, y: -13.31), controlPoint2: NSPoint(x: 16.14, y: -13.37)) + bezier2Path.curve(to: NSPoint(x: 16.65, y: -14.39), controlPoint1: NSPoint(x: 16.46, y: -14.07), controlPoint2: NSPoint(x: 16.54, y: -14.23)) + bezier2Path.curve(to: NSPoint(x: 17.14, y: -14.55), controlPoint1: NSPoint(x: 16.65, y: -14.41), controlPoint2: NSPoint(x: 16.83, y: -14.47)) + bezier2Path.line(to: NSPoint(x: 17.14, y: -14.55)) + bezier2Path.curve(to: NSPoint(x: 17.51, y: -14.66), controlPoint1: NSPoint(x: 17.34, y: -14.6), controlPoint2: NSPoint(x: 17.42, y: -14.63)) + bezier2Path.line(to: NSPoint(x: 17.57, y: -14.71)) + bezier2Path.curve(to: NSPoint(x: 17.92, y: -15.47), controlPoint1: NSPoint(x: 17.74, y: -15.09), controlPoint2: NSPoint(x: 17.74, y: -15.09)) + bezier2Path.line(to: NSPoint(x: 17.93, y: -15.53)) + bezier2Path.curve(to: NSPoint(x: 18.47, y: -15.96), controlPoint1: NSPoint(x: 17.88, y: -15.77), controlPoint2: NSPoint(x: 17.99, y: -15.85)) + bezier2Path.line(to: NSPoint(x: 18.47, y: -15.96)) + bezier2Path.curve(to: NSPoint(x: 18.97, y: -16.13), controlPoint1: NSPoint(x: 18.74, y: -16.02), controlPoint2: NSPoint(x: 18.86, y: -16.06)) + bezier2Path.curve(to: NSPoint(x: 19.37, y: -16.9), controlPoint1: NSPoint(x: 19.19, y: -16.39), controlPoint2: NSPoint(x: 19.32, y: -16.64)) + bezier2Path.curve(to: NSPoint(x: 19.26, y: -17.37), controlPoint1: NSPoint(x: 19.32, y: -17.16), controlPoint2: NSPoint(x: 19.32, y: -17.16)) + bezier2Path.curve(to: NSPoint(x: 19.24, y: -17.46), controlPoint1: NSPoint(x: 19.25, y: -17.41), controlPoint2: NSPoint(x: 19.25, y: -17.41)) + bezier2Path.curve(to: NSPoint(x: 19.16, y: -17.8), controlPoint1: NSPoint(x: 19.2, y: -17.63), controlPoint2: NSPoint(x: 19.2, y: -17.63)) + bezier2Path.line(to: NSPoint(x: 19.11, y: -17.69)) + bezier2Path.curve(to: NSPoint(x: 19.4, y: -17.88), controlPoint1: NSPoint(x: 19.26, y: -17.78), controlPoint2: NSPoint(x: 19.26, y: -17.78)) + bezier2Path.curve(to: NSPoint(x: 19.69, y: -18.08), controlPoint1: NSPoint(x: 19.54, y: -17.98), controlPoint2: NSPoint(x: 19.54, y: -17.98)) + bezier2Path.curve(to: NSPoint(x: 20.01, y: -18.79), controlPoint1: NSPoint(x: 19.91, y: -18.36), controlPoint2: NSPoint(x: 20.01, y: -18.58)) + bezier2Path.curve(to: NSPoint(x: 19.97, y: -19), controlPoint1: NSPoint(x: 19.99, y: -18.9), controlPoint2: NSPoint(x: 19.99, y: -18.9)) + bezier2Path.curve(to: NSPoint(x: 19.91, y: -19.28), controlPoint1: NSPoint(x: 19.94, y: -19.14), controlPoint2: NSPoint(x: 19.94, y: -19.14)) + bezier2Path.curve(to: NSPoint(x: 19.82, y: -19.75), controlPoint1: NSPoint(x: 19.87, y: -19.52), controlPoint2: NSPoint(x: 19.87, y: -19.52)) + bezier2Path.line(to: NSPoint(x: 19.77, y: -19.64)) + bezier2Path.curve(to: NSPoint(x: 20.03, y: -19.78), controlPoint1: NSPoint(x: 19.9, y: -19.71), controlPoint2: NSPoint(x: 19.9, y: -19.71)) + bezier2Path.curve(to: NSPoint(x: 20.05, y: -19.79), controlPoint1: NSPoint(x: 20.04, y: -19.78), controlPoint2: NSPoint(x: 20.04, y: -19.78)) + bezier2Path.curve(to: NSPoint(x: 20.34, y: -19.94), controlPoint1: NSPoint(x: 20.2, y: -19.86), controlPoint2: NSPoint(x: 20.2, y: -19.86)) + bezier2Path.curve(to: NSPoint(x: 20.73, y: -20.76), controlPoint1: NSPoint(x: 20.56, y: -20.21), controlPoint2: NSPoint(x: 20.67, y: -20.48)) + bezier2Path.curve(to: NSPoint(x: 20.63, y: -21.12), controlPoint1: NSPoint(x: 20.68, y: -20.96), controlPoint2: NSPoint(x: 20.68, y: -20.96)) + bezier2Path.curve(to: NSPoint(x: 20.59, y: -21.25), controlPoint1: NSPoint(x: 20.61, y: -21.18), controlPoint2: NSPoint(x: 20.61, y: -21.18)) + bezier2Path.curve(to: NSPoint(x: 20.52, y: -21.46), controlPoint1: NSPoint(x: 20.55, y: -21.36), controlPoint2: NSPoint(x: 20.55, y: -21.36)) + bezier2Path.line(to: NSPoint(x: 20.48, y: -21.35)) + bezier2Path.curve(to: NSPoint(x: 22.92, y: -22.2), controlPoint1: NSPoint(x: 21.25, y: -21.88), controlPoint2: NSPoint(x: 21.98, y: -22.16)) + bezier2Path.line(to: NSPoint(x: 22.82, y: -22.27)) + bezier2Path.curve(to: NSPoint(x: 23.13, y: -21.66), controlPoint1: NSPoint(x: 22.88, y: -22.08), controlPoint2: NSPoint(x: 22.98, y: -21.88)) + bezier2Path.curve(to: NSPoint(x: 23.68, y: -20.94), controlPoint1: NSPoint(x: 23.26, y: -21.46), controlPoint2: NSPoint(x: 23.38, y: -21.31)) + bezier2Path.line(to: NSPoint(x: 23.67, y: -21.06)) + bezier2Path.curve(to: NSPoint(x: 21.32, y: -16.62), controlPoint1: NSPoint(x: 22.49, y: -18.84), controlPoint2: NSPoint(x: 22.49, y: -18.84)) + bezier2Path.curve(to: NSPoint(x: 20.92, y: -15.88), controlPoint1: NSPoint(x: 21.12, y: -16.25), controlPoint2: NSPoint(x: 21.12, y: -16.25)) + bezier2Path.curve(to: NSPoint(x: 18.18, y: -10.7), controlPoint1: NSPoint(x: 19.55, y: -13.29), controlPoint2: NSPoint(x: 19.55, y: -13.29)) + bezier2Path.curve(to: NSPoint(x: 18.08, y: -8.82), controlPoint1: NSPoint(x: 17.94, y: -10.08), controlPoint2: NSPoint(x: 17.9, y: -9.45)) + bezier2Path.curve(to: NSPoint(x: 18.46, y: -3.32), controlPoint1: NSPoint(x: 19.24, y: -7.2), controlPoint2: NSPoint(x: 19.4, y: -5.09)) + bezier2Path.curve(to: NSPoint(x: 13.12, y: -0.6), controlPoint1: NSPoint(x: 17.42, y: -1.35), controlPoint2: NSPoint(x: 15.27, y: -0.28)) + bezier2Path.curve(to: NSPoint(x: 12.05, y: -0.49), controlPoint1: NSPoint(x: 12.75, y: -0.53), controlPoint2: NSPoint(x: 12.4, y: -0.49)) + bezier2Path.curve(to: NSPoint(x: 11.08, y: -0.59), controlPoint1: NSPoint(x: 11.72, y: -0.49), controlPoint2: NSPoint(x: 11.4, y: -0.52)) + bezier2Path.curve(to: NSPoint(x: 5.87, y: -3.36), controlPoint1: NSPoint(x: 8.94, y: -0.35), controlPoint2: NSPoint(x: 6.87, y: -1.43)) + bezier2Path.curve(to: NSPoint(x: 6.24, y: -8.79), controlPoint1: NSPoint(x: 4.95, y: -5.12), controlPoint2: NSPoint(x: 5.11, y: -7.22)) + bezier2Path.line(to: NSPoint(x: 6.25, y: -8.89)) + bezier2Path.curve(to: NSPoint(x: 5.65, y: -9.64), controlPoint1: NSPoint(x: 6.13, y: -9.21), controlPoint2: NSPoint(x: 5.94, y: -9.43)) + bezier2Path.curve(to: NSPoint(x: 5.33, y: -9.86), controlPoint1: NSPoint(x: 5.58, y: -9.69), controlPoint2: NSPoint(x: 5.35, y: -9.84)) + bezier2Path.curve(to: NSPoint(x: 5.01, y: -10.32), controlPoint1: NSPoint(x: 5.09, y: -10.02), controlPoint2: NSPoint(x: 5, y: -10.14)) + bezier2Path.curve(to: NSPoint(x: 5.18, y: -10.66), controlPoint1: NSPoint(x: 4.98, y: -10.5), controlPoint2: NSPoint(x: 5.02, y: -10.56)) + bezier2Path.curve(to: NSPoint(x: 5.35, y: -10.77), controlPoint1: NSPoint(x: 5.15, y: -10.64), controlPoint2: NSPoint(x: 5.31, y: -10.74)) + bezier2Path.curve(to: NSPoint(x: 5.7, y: -11.17), controlPoint1: NSPoint(x: 5.51, y: -10.88), controlPoint2: NSPoint(x: 5.62, y: -11)) + bezier2Path.line(to: NSPoint(x: 5.7, y: -11.25)) + bezier2Path.curve(to: NSPoint(x: 5.26, y: -12.12), controlPoint1: NSPoint(x: 5.6, y: -11.53), controlPoint2: NSPoint(x: 5.48, y: -11.77)) + bezier2Path.curve(to: NSPoint(x: 5.08, y: -12.43), controlPoint1: NSPoint(x: 5.17, y: -12.28), controlPoint2: NSPoint(x: 5.12, y: -12.36)) + bezier2Path.curve(to: NSPoint(x: 4.84, y: -12.9), controlPoint1: NSPoint(x: 4.97, y: -12.61), controlPoint2: NSPoint(x: 4.9, y: -12.76)) + bezier2Path.curve(to: NSPoint(x: 4.99, y: -13.4), controlPoint1: NSPoint(x: 4.83, y: -12.92), controlPoint2: NSPoint(x: 4.88, y: -13.1)) + bezier2Path.line(to: NSPoint(x: 4.99, y: -13.4)) + bezier2Path.curve(to: NSPoint(x: 5.11, y: -13.76), controlPoint1: NSPoint(x: 5.06, y: -13.59), controlPoint2: NSPoint(x: 5.08, y: -13.67)) + bezier2Path.line(to: NSPoint(x: 5.1, y: -13.84)) + bezier2Path.curve(to: NSPoint(x: 4.79, y: -14.36), controlPoint1: NSPoint(x: 4.95, y: -14.1), controlPoint2: NSPoint(x: 4.95, y: -14.1)) + bezier2Path.curve(to: NSPoint(x: 4.78, y: -14.38), controlPoint1: NSPoint(x: 4.79, y: -14.37), controlPoint2: NSPoint(x: 4.79, y: -14.37)) + bezier2Path.curve(to: NSPoint(x: 4.67, y: -14.56), controlPoint1: NSPoint(x: 4.73, y: -14.47), controlPoint2: NSPoint(x: 4.73, y: -14.47)) + bezier2Path.line(to: NSPoint(x: 4.62, y: -14.6)) + bezier2Path.curve(to: NSPoint(x: 4.58, y: -15.29), controlPoint1: NSPoint(x: 4.4, y: -14.7), controlPoint2: NSPoint(x: 4.4, y: -14.84)) + bezier2Path.line(to: NSPoint(x: 4.58, y: -15.29)) + bezier2Path.curve(to: NSPoint(x: 4.73, y: -15.8), controlPoint1: NSPoint(x: 4.69, y: -15.55), controlPoint2: NSPoint(x: 4.72, y: -15.67)) + bezier2Path.curve(to: NSPoint(x: 4.32, y: -16.57), controlPoint1: NSPoint(x: 4.64, y: -16.13), controlPoint2: NSPoint(x: 4.51, y: -16.38)) + bezier2Path.curve(to: NSPoint(x: 3.46, y: -16.9), controlPoint1: NSPoint(x: 3.87, y: -16.75), controlPoint2: NSPoint(x: 3.87, y: -16.75)) + bezier2Path.line(to: NSPoint(x: 3.52, y: -16.8)) + bezier2Path.curve(to: NSPoint(x: 3.53, y: -16.94), controlPoint1: NSPoint(x: 3.53, y: -16.87), controlPoint2: NSPoint(x: 3.53, y: -16.87)) + bezier2Path.curve(to: NSPoint(x: 3.53, y: -17.5), controlPoint1: NSPoint(x: 3.53, y: -17.22), controlPoint2: NSPoint(x: 3.53, y: -17.22)) + bezier2Path.line(to: NSPoint(x: 3.53, y: -17.5)) + bezier2Path.curve(to: NSPoint(x: 3.12, y: -18.16), controlPoint1: NSPoint(x: 3.42, y: -17.84), controlPoint2: NSPoint(x: 3.29, y: -18.05)) + bezier2Path.curve(to: NSPoint(x: 2.22, y: -18.56), controlPoint1: NSPoint(x: 2.66, y: -18.36), controlPoint2: NSPoint(x: 2.66, y: -18.36)) + bezier2Path.line(to: NSPoint(x: 2.22, y: -18.56)) + bezier2Path.line(to: NSPoint(x: 2.28, y: -18.45)) + bezier2Path.curve(to: NSPoint(x: 2.32, y: -18.76), controlPoint1: NSPoint(x: 2.3, y: -18.6), controlPoint2: NSPoint(x: 2.3, y: -18.6)) + bezier2Path.curve(to: NSPoint(x: 2.35, y: -18.93), controlPoint1: NSPoint(x: 2.34, y: -18.84), controlPoint2: NSPoint(x: 2.34, y: -18.84)) + bezier2Path.curve(to: NSPoint(x: 2.37, y: -19.09), controlPoint1: NSPoint(x: 2.36, y: -19.01), controlPoint2: NSPoint(x: 2.36, y: -19.01)) + bezier2Path.curve(to: NSPoint(x: 1.91, y: -19.87), controlPoint1: NSPoint(x: 2.26, y: -19.42), controlPoint2: NSPoint(x: 2.11, y: -19.67)) + bezier2Path.curve(to: NSPoint(x: 1.54, y: -20), controlPoint1: NSPoint(x: 1.7, y: -19.95), controlPoint2: NSPoint(x: 1.7, y: -19.95)) + bezier2Path.curve(to: NSPoint(x: 1.21, y: -20.1), controlPoint1: NSPoint(x: 1.37, y: -20.05), controlPoint2: NSPoint(x: 1.37, y: -20.05)) + bezier2Path.line(to: NSPoint(x: 1.28, y: -20.01)) + bezier2Path.curve(to: NSPoint(x: 1.97, y: -22.5), controlPoint1: NSPoint(x: 1.28, y: -20.94), controlPoint2: NSPoint(x: 1.47, y: -21.7)) + bezier2Path.line(to: NSPoint(x: 1.85, y: -22.46)) + bezier2Path.curve(to: NSPoint(x: 3.43, y: -22.41), controlPoint1: NSPoint(x: 2.23, y: -22.34), controlPoint2: NSPoint(x: 2.57, y: -22.34)) + bezier2Path.line(to: NSPoint(x: 3.33, y: -22.46)) + bezier2Path.curve(to: NSPoint(x: 8.75, y: -12.07), controlPoint1: NSPoint(x: 6.04, y: -17.27), controlPoint2: NSPoint(x: 6.04, y: -17.27)) + bezier2Path.curve(to: NSPoint(x: 9.37, y: -11.39), controlPoint1: NSPoint(x: 8.93, y: -11.8), controlPoint2: NSPoint(x: 9.14, y: -11.58)) + bezier2Path.close() + bezier2Path.move(to: NSPoint(x: 13.31, y: -2.15)) + bezier2Path.curve(to: NSPoint(x: 14.95, y: -3.78), controlPoint1: NSPoint(x: 14.22, y: -2.15), controlPoint2: NSPoint(x: 14.95, y: -2.88)) + bezier2Path.curve(to: NSPoint(x: 13.31, y: -5.42), controlPoint1: NSPoint(x: 14.95, y: -4.69), controlPoint2: NSPoint(x: 14.22, y: -5.42)) + bezier2Path.curve(to: NSPoint(x: 11.67, y: -3.78), controlPoint1: NSPoint(x: 12.41, y: -5.42), controlPoint2: NSPoint(x: 11.67, y: -4.69)) + bezier2Path.curve(to: NSPoint(x: 13.31, y: -2.15), controlPoint1: NSPoint(x: 11.67, y: -2.88), controlPoint2: NSPoint(x: 12.41, y: -2.15)) + bezier2Path.close() + bezier2Path.move(to: NSPoint(x: 13.31, y: -2.35)) + bezier2Path.curve(to: NSPoint(x: 11.88, y: -3.78), controlPoint1: NSPoint(x: 12.52, y: -2.35), controlPoint2: NSPoint(x: 11.88, y: -2.99)) + bezier2Path.curve(to: NSPoint(x: 13.31, y: -5.22), controlPoint1: NSPoint(x: 11.88, y: -4.57), controlPoint2: NSPoint(x: 12.52, y: -5.22)) + bezier2Path.curve(to: NSPoint(x: 14.74, y: -3.78), controlPoint1: NSPoint(x: 14.1, y: -5.22), controlPoint2: NSPoint(x: 14.74, y: -4.57)) + bezier2Path.curve(to: NSPoint(x: 13.31, y: -2.35), controlPoint1: NSPoint(x: 14.74, y: -2.99), controlPoint2: NSPoint(x: 14.1, y: -2.35)) + bezier2Path.close() + setupKeyFill.setFill() + bezier2Path.fill() + + + + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawUnlockButton(frame: NSRect = NSRect(x: 0, y: 0, width: 230, height: 230)) { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Color Declarations + let unlockColor = NSColor(red: 0, green: 0, blue: 0, alpha: 1) + + //// Group + NSGraphicsContext.saveGraphicsState() + context.translateBy(x: frame.minX + 0.14000 * frame.width, y: frame.minY + 0.02000 * frame.height) + context.scaleBy(x: 4.6, y: 4.6) + + + + //// Rectangle Drawing + let rectanglePath = NSBezierPath(roundedRect: NSRect(x: 0, y: -0, width: 36, height: 28), xRadius: 2, yRadius: 2) + unlockColor.setStroke() + rectanglePath.lineWidth = 2 + rectanglePath.stroke() + + + //// Bezier Drawing + let bezierPath = NSBezierPath() + bezierPath.move(to: NSPoint(x: 29, y: 28)) + bezierPath.curve(to: NSPoint(x: 29, y: 34), controlPoint1: NSPoint(x: 29, y: 28), controlPoint2: NSPoint(x: 29, y: 32.9)) + bezierPath.curve(to: NSPoint(x: 18, y: 45), controlPoint1: NSPoint(x: 29, y: 40.1), controlPoint2: NSPoint(x: 24.1, y: 45)) + bezierPath.curve(to: NSPoint(x: 7, y: 34), controlPoint1: NSPoint(x: 11.9, y: 45), controlPoint2: NSPoint(x: 7, y: 40.1)) + bezierPath.curve(to: NSPoint(x: 7, y: 28), controlPoint1: NSPoint(x: 7, y: 32.9), controlPoint2: NSPoint(x: 7, y: 28)) + unlockColor.setStroke() + bezierPath.lineWidth = 2 + bezierPath.lineCapStyle = .round + bezierPath.stroke() + + + //// Bezier 2 Drawing + let bezier2Path = NSBezierPath() + bezier2Path.move(to: NSPoint(x: 21, y: 16)) + bezier2Path.curve(to: NSPoint(x: 18, y: 19), controlPoint1: NSPoint(x: 21, y: 17.7), controlPoint2: NSPoint(x: 19.7, y: 19)) + bezier2Path.curve(to: NSPoint(x: 15, y: 16), controlPoint1: NSPoint(x: 16.3, y: 19), controlPoint2: NSPoint(x: 15, y: 17.7)) + bezier2Path.curve(to: NSPoint(x: 16, y: 13.8), controlPoint1: NSPoint(x: 15, y: 15.1), controlPoint2: NSPoint(x: 15.4, y: 14.3)) + bezier2Path.line(to: NSPoint(x: 16, y: 11)) + bezier2Path.curve(to: NSPoint(x: 18, y: 9), controlPoint1: NSPoint(x: 16, y: 9.9), controlPoint2: NSPoint(x: 16.9, y: 9)) + bezier2Path.curve(to: NSPoint(x: 20, y: 11), controlPoint1: NSPoint(x: 19.1, y: 9), controlPoint2: NSPoint(x: 20, y: 9.9)) + bezier2Path.line(to: NSPoint(x: 20, y: 13.8)) + bezier2Path.curve(to: NSPoint(x: 21, y: 16), controlPoint1: NSPoint(x: 20.6, y: 14.3), controlPoint2: NSPoint(x: 21, y: 15.1)) + bezier2Path.close() + unlockColor.setFill() + bezier2Path.fill() + + + + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawUnlockButtonSelected(frame: NSRect = NSRect(x: 0, y: 0, width: 230, height: 230)) { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Color Declarations + let unlockColor = NSColor(red: 0, green: 0, blue: 0, alpha: 1) + + //// Group 2 + NSGraphicsContext.saveGraphicsState() + context.translateBy(x: frame.minX + 0.00000 * frame.width, y: frame.minY + 1.00000 * frame.height) + context.scaleBy(x: 4.6, y: 4.6) + + + + //// Group + //// Rectangle Drawing + let rectanglePath = NSBezierPath(roundedRect: NSRect(x: 7, y: -48.99, width: 36, height: 28), xRadius: 2, yRadius: 2) + unlockColor.setStroke() + rectanglePath.lineWidth = 2 + rectanglePath.stroke() + + + //// Bezier Drawing + let bezierPath = NSBezierPath() + bezierPath.move(to: NSPoint(x: 34.6, y: -13.09)) + bezierPath.curve(to: NSPoint(x: 33.3, y: -8.79), controlPoint1: NSPoint(x: 34.6, y: -13.09), controlPoint2: NSPoint(x: 33.5, y: -9.49)) + bezierPath.curve(to: NSPoint(x: 19.5, y: -1.49), controlPoint1: NSPoint(x: 31.5, y: -2.99), controlPoint2: NSPoint(x: 25.3, y: 0.31)) + bezierPath.curve(to: NSPoint(x: 12.2, y: -15.29), controlPoint1: NSPoint(x: 13.7, y: -3.29), controlPoint2: NSPoint(x: 10.4, y: -9.49)) + bezierPath.curve(to: NSPoint(x: 14, y: -20.99), controlPoint1: NSPoint(x: 12.6, y: -16.39), controlPoint2: NSPoint(x: 14, y: -20.99)) + unlockColor.setStroke() + bezierPath.lineWidth = 2 + bezierPath.lineCapStyle = .round + bezierPath.stroke() + + + //// Bezier 2 Drawing + let bezier2Path = NSBezierPath() + bezier2Path.move(to: NSPoint(x: 28, y: -32.99)) + bezier2Path.curve(to: NSPoint(x: 25, y: -29.99), controlPoint1: NSPoint(x: 28, y: -31.29), controlPoint2: NSPoint(x: 26.7, y: -29.99)) + bezier2Path.curve(to: NSPoint(x: 22, y: -32.99), controlPoint1: NSPoint(x: 23.3, y: -29.99), controlPoint2: NSPoint(x: 22, y: -31.29)) + bezier2Path.curve(to: NSPoint(x: 23, y: -35.19), controlPoint1: NSPoint(x: 22, y: -33.89), controlPoint2: NSPoint(x: 22.4, y: -34.69)) + bezier2Path.line(to: NSPoint(x: 23, y: -37.99)) + bezier2Path.curve(to: NSPoint(x: 25, y: -39.99), controlPoint1: NSPoint(x: 23, y: -39.09), controlPoint2: NSPoint(x: 23.9, y: -39.99)) + bezier2Path.curve(to: NSPoint(x: 27, y: -37.99), controlPoint1: NSPoint(x: 26.1, y: -39.99), controlPoint2: NSPoint(x: 27, y: -39.09)) + bezier2Path.line(to: NSPoint(x: 27, y: -35.19)) + bezier2Path.curve(to: NSPoint(x: 28, y: -32.99), controlPoint1: NSPoint(x: 27.6, y: -34.69), controlPoint2: NSPoint(x: 28, y: -33.89)) + bezier2Path.close() + unlockColor.setFill() + bezier2Path.fill() + + + + + + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawPermissionBadgeOwner(frame: NSRect = NSRect(x: 0, y: 0, width: 51, height: 51)) { + + //// Bezier Drawing + let bezierPath = NSBezierPath() + bezierPath.move(to: NSPoint(x: frame.minX + 0.49750 * frame.width, y: frame.minY + 0.99749 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.99751 * frame.width, y: frame.minY + 0.49749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.77365 * frame.width, y: frame.minY + 0.99749 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.99751 * frame.width, y: frame.minY + 0.77363 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.50001 * frame.width, y: frame.minY + 0.00250 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.99751 * frame.width, y: frame.minY + 0.22135 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.77478 * frame.width, y: frame.minY + 0.00250 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.00251 * frame.width, y: frame.minY + 0.49749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.22525 * frame.width, y: frame.minY + 0.00250 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.00251 * frame.width, y: frame.minY + 0.22135 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.49750 * frame.width, y: frame.minY + 0.99749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.00251 * frame.width, y: frame.minY + 0.77363 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.22135 * frame.width, y: frame.minY + 0.99749 * frame.height)) + bezierPath.close() + bezierPath.move(to: NSPoint(x: frame.minX + 0.58089 * frame.width, y: frame.minY + 0.80390 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.57086 * frame.width, y: frame.minY + 0.74047 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.57754 * frame.width, y: frame.minY + 0.78276 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.57420 * frame.width, y: frame.minY + 0.76162 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.63183 * frame.width, y: frame.minY + 0.71281 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.59227 * frame.width, y: frame.minY + 0.73637 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.61306 * frame.width, y: frame.minY + 0.72598 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.59036 * frame.width, y: frame.minY + 0.65504 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.63938 * frame.width, y: frame.minY + 0.61460 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.68279 * frame.width, y: frame.minY + 0.67087 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.72865 * frame.width, y: frame.minY + 0.60644 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.70229 * frame.width, y: frame.minY + 0.65313 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.71961 * frame.width, y: frame.minY + 0.62781 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.78109 * frame.width, y: frame.minY + 0.64054 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.72877 * frame.width, y: frame.minY + 0.60618 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.77331 * frame.width, y: frame.minY + 0.63548 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.72915 * frame.width, y: frame.minY + 0.71280 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.77096 * frame.width, y: frame.minY + 0.66543 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.75222 * frame.width, y: frame.minY + 0.68953 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.80054 * frame.width, y: frame.minY + 0.72858 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.79038 * frame.width, y: frame.minY + 0.78205 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.71567 * frame.width, y: frame.minY + 0.76458 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.71721 * frame.width, y: frame.minY + 0.85113 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.71567 * frame.width, y: frame.minY + 0.79052 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.71721 * frame.width, y: frame.minY + 0.82519 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.65838 * frame.width, y: frame.minY + 0.85297 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.69760 * frame.width, y: frame.minY + 0.85174 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.67799 * frame.width, y: frame.minY + 0.85236 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.65823 * frame.width, y: frame.minY + 0.76836 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.65950 * frame.width, y: frame.minY + 0.82507 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.65802 * frame.width, y: frame.minY + 0.79627 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.58285 * frame.width, y: frame.minY + 0.80338 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.63199 * frame.width, y: frame.minY + 0.78449 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.60920 * frame.width, y: frame.minY + 0.79734 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.58285 * frame.width, y: frame.minY + 0.80336 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.58089 * frame.width, y: frame.minY + 0.80390 * frame.height)) + bezierPath.close() + bezierPath.move(to: NSPoint(x: frame.minX + 0.79977 * frame.width, y: frame.minY + 0.59489 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.74253 * frame.width, y: frame.minY + 0.56373 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.75127 * frame.width, y: frame.minY + 0.49749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.74823 * frame.width, y: frame.minY + 0.54262 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.75127 * frame.width, y: frame.minY + 0.52041 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.49750 * frame.width, y: frame.minY + 0.24372 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.75127 * frame.width, y: frame.minY + 0.35733 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.63766 * frame.width, y: frame.minY + 0.24372 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.24372 * frame.width, y: frame.minY + 0.49749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.35734 * frame.width, y: frame.minY + 0.24372 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.24372 * frame.width, y: frame.minY + 0.35733 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.49750 * frame.width, y: frame.minY + 0.75126 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.24372 * frame.width, y: frame.minY + 0.63764 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.35734 * frame.width, y: frame.minY + 0.75126 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.52614 * frame.width, y: frame.minY + 0.74965 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.50718 * frame.width, y: frame.minY + 0.75126 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.51674 * frame.width, y: frame.minY + 0.75071 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.53047 * frame.width, y: frame.minY + 0.81329 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.49750 * frame.width, y: frame.minY + 0.81499 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.51963 * frame.width, y: frame.minY + 0.81441 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.50863 * frame.width, y: frame.minY + 0.81499 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.17999 * frame.width, y: frame.minY + 0.49749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.32215 * frame.width, y: frame.minY + 0.81499 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.17999 * frame.width, y: frame.minY + 0.67284 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.49750 * frame.width, y: frame.minY + 0.17999 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.17999 * frame.width, y: frame.minY + 0.32214 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.32215 * frame.width, y: frame.minY + 0.17999 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.81501 * frame.width, y: frame.minY + 0.49749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.67285 * frame.width, y: frame.minY + 0.17999 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.81501 * frame.width, y: frame.minY + 0.32214 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.79977 * frame.width, y: frame.minY + 0.59489 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.81501 * frame.width, y: frame.minY + 0.53147 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.80966 * frame.width, y: frame.minY + 0.56420 * frame.height)) + bezierPath.close() + bezierPath.move(to: NSPoint(x: frame.minX + 0.49968 * frame.width, y: frame.minY + 0.63779 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.57106 * frame.width, y: frame.minY + 0.56641 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.53910 * frame.width, y: frame.minY + 0.63779 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.57106 * frame.width, y: frame.minY + 0.60583 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.53407 * frame.width, y: frame.minY + 0.50386 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.57106 * frame.width, y: frame.minY + 0.53946 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.55612 * frame.width, y: frame.minY + 0.51601 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.56925 * frame.width, y: frame.minY + 0.35776 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.42279 * frame.width, y: frame.minY + 0.35776 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.46477 * frame.width, y: frame.minY + 0.50415 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.42830 * frame.width, y: frame.minY + 0.56641 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.44301 * frame.width, y: frame.minY + 0.51638 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.42830 * frame.width, y: frame.minY + 0.53968 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.49968 * frame.width, y: frame.minY + 0.63779 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.42830 * frame.width, y: frame.minY + 0.60583 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.46027 * frame.width, y: frame.minY + 0.63779 * frame.height)) + bezierPath.close() + bezierPath.windingRule = .evenOdd + StyleKit.wirelessBlue.setFill() + bezierPath.fill() + } + + @objc dynamic public class func drawPermissionBadgeScheduled(frame: NSRect = NSRect(x: 0, y: 0, width: 51, height: 51)) { + + //// Beizer Drawing + let beizerPath = NSBezierPath() + beizerPath.move(to: NSPoint(x: frame.minX + 0.49749 * frame.width, y: frame.minY + 0.99750 * frame.height)) + beizerPath.curve(to: NSPoint(x: frame.minX + 0.99750 * frame.width, y: frame.minY + 0.50000 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.77364 * frame.width, y: frame.minY + 0.99750 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.99750 * frame.width, y: frame.minY + 0.77476 * frame.height)) + beizerPath.curve(to: NSPoint(x: frame.minX + 0.50251 * frame.width, y: frame.minY + 0.00250 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.99750 * frame.width, y: frame.minY + 0.22524 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.77866 * frame.width, y: frame.minY + 0.00250 * frame.height)) + beizerPath.curve(to: NSPoint(x: frame.minX + 0.00250 * frame.width, y: frame.minY + 0.50000 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.22636 * frame.width, y: frame.minY + 0.00250 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.00250 * frame.width, y: frame.minY + 0.22524 * frame.height)) + beizerPath.curve(to: NSPoint(x: frame.minX + 0.49749 * frame.width, y: frame.minY + 0.99750 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.00250 * frame.width, y: frame.minY + 0.77476 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.22134 * frame.width, y: frame.minY + 0.99750 * frame.height)) + beizerPath.close() + beizerPath.move(to: NSPoint(x: frame.minX + 0.50313 * frame.width, y: frame.minY + 0.75005 * frame.height)) + beizerPath.curve(to: NSPoint(x: frame.minX + 0.76008 * frame.width, y: frame.minY + 0.49439 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.64504 * frame.width, y: frame.minY + 0.75005 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.76008 * frame.width, y: frame.minY + 0.63558 * frame.height)) + beizerPath.curve(to: NSPoint(x: frame.minX + 0.50313 * frame.width, y: frame.minY + 0.23873 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.76008 * frame.width, y: frame.minY + 0.35319 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.64504 * frame.width, y: frame.minY + 0.23873 * frame.height)) + beizerPath.curve(to: NSPoint(x: frame.minX + 0.24618 * frame.width, y: frame.minY + 0.49439 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.36122 * frame.width, y: frame.minY + 0.23873 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.24618 * frame.width, y: frame.minY + 0.35319 * frame.height)) + beizerPath.curve(to: NSPoint(x: frame.minX + 0.50313 * frame.width, y: frame.minY + 0.75005 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.24618 * frame.width, y: frame.minY + 0.63558 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.36122 * frame.width, y: frame.minY + 0.75005 * frame.height)) + beizerPath.close() + beizerPath.move(to: NSPoint(x: frame.minX + 0.47356 * frame.width, y: frame.minY + 0.68359 * frame.height)) + beizerPath.line(to: NSPoint(x: frame.minX + 0.53326 * frame.width, y: frame.minY + 0.68359 * frame.height)) + beizerPath.line(to: NSPoint(x: frame.minX + 0.53326 * frame.width, y: frame.minY + 0.45340 * frame.height)) + beizerPath.line(to: NSPoint(x: frame.minX + 0.51949 * frame.width, y: frame.minY + 0.45340 * frame.height)) + beizerPath.line(to: NSPoint(x: frame.minX + 0.33517 * frame.width, y: frame.minY + 0.45264 * frame.height)) + beizerPath.line(to: NSPoint(x: frame.minX + 0.33500 * frame.width, y: frame.minY + 0.51205 * frame.height)) + beizerPath.line(to: NSPoint(x: frame.minX + 0.47356 * frame.width, y: frame.minY + 0.51262 * frame.height)) + beizerPath.line(to: NSPoint(x: frame.minX + 0.47356 * frame.width, y: frame.minY + 0.68359 * frame.height)) + beizerPath.close() + beizerPath.move(to: NSPoint(x: frame.minX + 0.49749 * frame.width, y: frame.minY + 0.82286 * frame.height)) + beizerPath.curve(to: NSPoint(x: frame.minX + 0.82198 * frame.width, y: frame.minY + 0.50000 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.67670 * frame.width, y: frame.minY + 0.82286 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.82198 * frame.width, y: frame.minY + 0.67831 * frame.height)) + beizerPath.curve(to: NSPoint(x: frame.minX + 0.49749 * frame.width, y: frame.minY + 0.17714 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.82198 * frame.width, y: frame.minY + 0.32169 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.67670 * frame.width, y: frame.minY + 0.17714 * frame.height)) + beizerPath.curve(to: NSPoint(x: frame.minX + 0.17299 * frame.width, y: frame.minY + 0.50000 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.31828 * frame.width, y: frame.minY + 0.17714 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.17299 * frame.width, y: frame.minY + 0.32169 * frame.height)) + beizerPath.curve(to: NSPoint(x: frame.minX + 0.49749 * frame.width, y: frame.minY + 0.82286 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.17299 * frame.width, y: frame.minY + 0.67831 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.31828 * frame.width, y: frame.minY + 0.82286 * frame.height)) + beizerPath.close() + beizerPath.windingRule = .evenOdd + StyleKit.wirelessBlue.setFill() + beizerPath.fill() + } + + @objc dynamic public class func drawPermissionBadgeAnytime(frame: NSRect = NSRect(x: 0, y: 0, width: 51, height: 51)) { + + //// Bezier Drawing + let bezierPath = NSBezierPath() + bezierPath.move(to: NSPoint(x: frame.minX + 0.49750 * frame.width, y: frame.minY + 0.99749 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.99751 * frame.width, y: frame.minY + 0.49749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.77365 * frame.width, y: frame.minY + 0.99749 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.99751 * frame.width, y: frame.minY + 0.77363 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.50001 * frame.width, y: frame.minY + 0.00250 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.99751 * frame.width, y: frame.minY + 0.22135 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.77478 * frame.width, y: frame.minY + 0.00250 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.00251 * frame.width, y: frame.minY + 0.49749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.22525 * frame.width, y: frame.minY + 0.00250 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.00251 * frame.width, y: frame.minY + 0.22135 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.49750 * frame.width, y: frame.minY + 0.99749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.00251 * frame.width, y: frame.minY + 0.77363 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.22135 * frame.width, y: frame.minY + 0.99749 * frame.height)) + bezierPath.close() + bezierPath.move(to: NSPoint(x: frame.minX + 0.50314 * frame.width, y: frame.minY + 0.74969 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.76099 * frame.width, y: frame.minY + 0.49185 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.64555 * frame.width, y: frame.minY + 0.74969 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.76099 * frame.width, y: frame.minY + 0.63425 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.50314 * frame.width, y: frame.minY + 0.23401 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.76099 * frame.width, y: frame.minY + 0.34944 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.64555 * frame.width, y: frame.minY + 0.23401 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.24529 * frame.width, y: frame.minY + 0.49185 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.36073 * frame.width, y: frame.minY + 0.23401 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.24529 * frame.width, y: frame.minY + 0.34944 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.50314 * frame.width, y: frame.minY + 0.74969 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.24529 * frame.width, y: frame.minY + 0.63425 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.36073 * frame.width, y: frame.minY + 0.74969 * frame.height)) + bezierPath.close() + bezierPath.move(to: NSPoint(x: frame.minX + 0.53087 * frame.width, y: frame.minY + 0.53749 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.63468 * frame.width, y: frame.minY + 0.43537 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.63468 * frame.width, y: frame.minY + 0.36634 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.57560 * frame.width, y: frame.minY + 0.36758 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.57684 * frame.width, y: frame.minY + 0.41236 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.52958 * frame.width, y: frame.minY + 0.41360 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.53082 * frame.width, y: frame.minY + 0.45714 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.48729 * frame.width, y: frame.minY + 0.45590 * frame.height)) + bezierPath.line(to: NSPoint(x: frame.minX + 0.47026 * frame.width, y: frame.minY + 0.47293 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.44380 * frame.width, y: frame.minY + 0.46895 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.46190 * frame.width, y: frame.minY + 0.47034 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.45301 * frame.width, y: frame.minY + 0.46895 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.35425 * frame.width, y: frame.minY + 0.55850 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.39434 * frame.width, y: frame.minY + 0.46895 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.35425 * frame.width, y: frame.minY + 0.50904 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.44380 * frame.width, y: frame.minY + 0.64805 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.35425 * frame.width, y: frame.minY + 0.60796 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.39434 * frame.width, y: frame.minY + 0.64805 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.53336 * frame.width, y: frame.minY + 0.55850 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.49326 * frame.width, y: frame.minY + 0.64805 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.53336 * frame.width, y: frame.minY + 0.60796 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.53087 * frame.width, y: frame.minY + 0.53749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.53336 * frame.width, y: frame.minY + 0.55126 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.53250 * frame.width, y: frame.minY + 0.54422 * frame.height)) + bezierPath.close() + bezierPath.move(to: NSPoint(x: frame.minX + 0.42851 * frame.width, y: frame.minY + 0.60577 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.45743 * frame.width, y: frame.minY + 0.57623 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.44448 * frame.width, y: frame.minY + 0.60577 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.45743 * frame.width, y: frame.minY + 0.59254 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.42851 * frame.width, y: frame.minY + 0.54669 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.45743 * frame.width, y: frame.minY + 0.55992 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.44448 * frame.width, y: frame.minY + 0.54669 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.39960 * frame.width, y: frame.minY + 0.57623 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.41254 * frame.width, y: frame.minY + 0.54669 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.39960 * frame.width, y: frame.minY + 0.55992 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.42851 * frame.width, y: frame.minY + 0.60577 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.39960 * frame.width, y: frame.minY + 0.59254 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.41254 * frame.width, y: frame.minY + 0.60577 * frame.height)) + bezierPath.close() + bezierPath.move(to: NSPoint(x: frame.minX + 0.49750 * frame.width, y: frame.minY + 0.82197 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.82199 * frame.width, y: frame.minY + 0.49749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.67671 * frame.width, y: frame.minY + 0.82197 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.82199 * frame.width, y: frame.minY + 0.67669 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.49750 * frame.width, y: frame.minY + 0.17300 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.82199 * frame.width, y: frame.minY + 0.31828 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.67671 * frame.width, y: frame.minY + 0.17300 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.17301 * frame.width, y: frame.minY + 0.49749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.31829 * frame.width, y: frame.minY + 0.17300 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.17301 * frame.width, y: frame.minY + 0.31828 * frame.height)) + bezierPath.curve(to: NSPoint(x: frame.minX + 0.49750 * frame.width, y: frame.minY + 0.82197 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.17301 * frame.width, y: frame.minY + 0.67669 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.31829 * frame.width, y: frame.minY + 0.82197 * frame.height)) + bezierPath.close() + bezierPath.windingRule = .evenOdd + StyleKit.wirelessBlue.setFill() + bezierPath.fill() + } + + @objc dynamic public class func drawPermissionBadgeAdmin(frame: NSRect = NSRect(x: 0, y: 0, width: 51, height: 51)) { + + //// Bezier 2 Drawing + let bezier2Path = NSBezierPath() + bezier2Path.move(to: NSPoint(x: frame.minX + 0.49750 * frame.width, y: frame.minY + 0.99749 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.99751 * frame.width, y: frame.minY + 0.49749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.77365 * frame.width, y: frame.minY + 0.99749 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.99751 * frame.width, y: frame.minY + 0.77363 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.50001 * frame.width, y: frame.minY + 0.00250 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.99751 * frame.width, y: frame.minY + 0.22135 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.77478 * frame.width, y: frame.minY + 0.00250 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.00251 * frame.width, y: frame.minY + 0.49749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.22525 * frame.width, y: frame.minY + 0.00250 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.00251 * frame.width, y: frame.minY + 0.22135 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.49750 * frame.width, y: frame.minY + 0.99749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.00251 * frame.width, y: frame.minY + 0.77363 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.22135 * frame.width, y: frame.minY + 0.99749 * frame.height)) + bezier2Path.close() + bezier2Path.move(to: NSPoint(x: frame.minX + 0.49186 * frame.width, y: frame.minY + 0.75730 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.75168 * frame.width, y: frame.minY + 0.49749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.63535 * frame.width, y: frame.minY + 0.75730 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.75168 * frame.width, y: frame.minY + 0.64097 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.49186 * frame.width, y: frame.minY + 0.23768 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.75168 * frame.width, y: frame.minY + 0.35400 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.63535 * frame.width, y: frame.minY + 0.23768 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.23204 * frame.width, y: frame.minY + 0.49749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.34837 * frame.width, y: frame.minY + 0.23768 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.23204 * frame.width, y: frame.minY + 0.35400 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.49186 * frame.width, y: frame.minY + 0.75730 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.23204 * frame.width, y: frame.minY + 0.64097 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.34837 * frame.width, y: frame.minY + 0.75730 * frame.height)) + bezier2Path.close() + bezier2Path.move(to: NSPoint(x: frame.minX + 0.49435 * frame.width, y: frame.minY + 0.63320 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.56414 * frame.width, y: frame.minY + 0.56341 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.53289 * frame.width, y: frame.minY + 0.63320 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.56414 * frame.width, y: frame.minY + 0.60195 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.53079 * frame.width, y: frame.minY + 0.50388 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.56414 * frame.width, y: frame.minY + 0.53822 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.55079 * frame.width, y: frame.minY + 0.51615 * frame.height)) + bezier2Path.line(to: NSPoint(x: frame.minX + 0.56372 * frame.width, y: frame.minY + 0.35680 * frame.height)) + bezier2Path.line(to: NSPoint(x: frame.minX + 0.42566 * frame.width, y: frame.minY + 0.35680 * frame.height)) + bezier2Path.line(to: NSPoint(x: frame.minX + 0.45925 * frame.width, y: frame.minY + 0.50308 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.42455 * frame.width, y: frame.minY + 0.56341 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.43850 * frame.width, y: frame.minY + 0.51517 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.42455 * frame.width, y: frame.minY + 0.53766 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.49435 * frame.width, y: frame.minY + 0.63320 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.42455 * frame.width, y: frame.minY + 0.60195 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.45580 * frame.width, y: frame.minY + 0.63320 * frame.height)) + bezier2Path.close() + bezier2Path.move(to: NSPoint(x: frame.minX + 0.49750 * frame.width, y: frame.minY + 0.82197 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.82199 * frame.width, y: frame.minY + 0.49749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.67671 * frame.width, y: frame.minY + 0.82197 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.82199 * frame.width, y: frame.minY + 0.67669 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.49750 * frame.width, y: frame.minY + 0.17300 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.82199 * frame.width, y: frame.minY + 0.31828 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.67671 * frame.width, y: frame.minY + 0.17300 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.17301 * frame.width, y: frame.minY + 0.49749 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.31829 * frame.width, y: frame.minY + 0.17300 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.17301 * frame.width, y: frame.minY + 0.31828 * frame.height)) + bezier2Path.curve(to: NSPoint(x: frame.minX + 0.49750 * frame.width, y: frame.minY + 0.82197 * frame.height), controlPoint1: NSPoint(x: frame.minX + 0.17301 * frame.width, y: frame.minY + 0.67669 * frame.height), controlPoint2: NSPoint(x: frame.minX + 0.31829 * frame.width, y: frame.minY + 0.82197 * frame.height)) + bezier2Path.close() + bezier2Path.windingRule = .evenOdd + StyleKit.wirelessBlue.setFill() + bezier2Path.fill() + } + + @objc dynamic public class func drawWatchScan2() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 130, height: 130) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawScan2(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawWatchScan3() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 130, height: 130) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawScan3(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawWatchScan1() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 130, height: 130) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawScan1(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawWatchScan4() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 130, height: 130) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawScan4(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawWatchOwner() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 130, height: 130) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawPermissionBadgeOwner(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawWatchScheduled() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 130, height: 130) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawPermissionBadgeScheduled(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawWatchAdmin() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 130, height: 130) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawPermissionBadgeAdmin(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawWatchAnytime() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 130, height: 130) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawPermissionBadgeAnytime(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawModularComplicationPlaceholder38mm() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 52, height: 52) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawPermissionBadgeAdmin(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawWatchScheduled2() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 130, height: 130) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawPermissionBadgeScheduled(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawModularComplicationPlaceholder42mm() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 58, height: 58) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawPermissionBadgeAdmin(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawWatchAnytime2() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 130, height: 130) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawPermissionBadgeAnytime(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawModularSmallAdmin38() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 28, height: 28) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawPermissionBadgeAdmin(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawModularSmallAdmin42() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 30, height: 30) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawPermissionBadgeAdmin(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawModularSmallAnytime38() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 28, height: 28) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawPermissionBadgeAnytime(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawModularSmallAnytime42() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 30, height: 30) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawPermissionBadgeAnytime(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawModularSmallOwner38() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 28, height: 28) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawPermissionBadgeOwner(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawModularSmallOwner42() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 30, height: 30) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawPermissionBadgeOwner(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawModularSmallScheduled38() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 28, height: 28) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawPermissionBadgeScheduled(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawModularSmallScheduled42() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 30, height: 30) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawPermissionBadgeScheduled(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawModularSmallScan38() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 28, height: 28) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawScan4(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawModularSmallScan42() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 30, height: 30) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawScan4(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawModularSmallKey42() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 30, height: 30) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawModularSmallKey38(frame: CGRect(origin: .zero, size: symbolRect.size), resizing: .stretch) + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawModularSmallKey38(frame targetFrame: NSRect = NSRect(x: 0, y: 0, width: 28, height: 28), resizing: ResizingBehavior = .aspectFit) { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Resize to Target Frame + NSGraphicsContext.saveGraphicsState() + let resizedFrame: NSRect = resizing.apply(rect: NSRect(x: 0, y: 0, width: 28, height: 28), target: targetFrame) + context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY) + context.scaleBy(x: resizedFrame.width / 28, y: resizedFrame.height / 28) + + + //// Color Declarations + let black = NSColor(red: 0, green: 0, blue: 0, alpha: 1) + + //// Symbol Drawing + let symbolRect = NSRect(x: 0, y: 0, width: 28, height: 28) + NSGraphicsContext.saveGraphicsState() + symbolRect.clip() + context.translateBy(x: symbolRect.minX, y: symbolRect.minY) + + StyleKit.drawUnlockButtonSelected(frame: NSRect(x: 0, y: 0, width: symbolRect.width, height: symbolRect.height)) + NSGraphicsContext.restoreGraphicsState() + + + //// key.svg Group + //// Group + //// Bezier Drawing + let bezierPath = NSBezierPath() + bezierPath.move(to: NSPoint(x: 10.25, y: 26.5)) + bezierPath.curve(to: NSPoint(x: 1.5, y: 17.75), controlPoint1: NSPoint(x: 5.38, y: 26.5), controlPoint2: NSPoint(x: 1.5, y: 22.62)) + bezierPath.curve(to: NSPoint(x: 10.25, y: 9), controlPoint1: NSPoint(x: 1.5, y: 12.88), controlPoint2: NSPoint(x: 5.38, y: 9)) + bezierPath.curve(to: NSPoint(x: 19, y: 17.75), controlPoint1: NSPoint(x: 15.12, y: 9), controlPoint2: NSPoint(x: 19, y: 12.88)) + bezierPath.curve(to: NSPoint(x: 10.25, y: 26.5), controlPoint1: NSPoint(x: 19, y: 22.62), controlPoint2: NSPoint(x: 15.12, y: 26.5)) + bezierPath.close() + bezierPath.move(to: NSPoint(x: 8.38, y: 16.5)) + bezierPath.curve(to: NSPoint(x: 5.25, y: 19.62), controlPoint1: NSPoint(x: 6.62, y: 16.5), controlPoint2: NSPoint(x: 5.25, y: 17.88)) + bezierPath.curve(to: NSPoint(x: 8.38, y: 22.75), controlPoint1: NSPoint(x: 5.25, y: 21.38), controlPoint2: NSPoint(x: 6.62, y: 22.75)) + bezierPath.curve(to: NSPoint(x: 11.5, y: 19.62), controlPoint1: NSPoint(x: 10.12, y: 22.75), controlPoint2: NSPoint(x: 11.5, y: 21.38)) + bezierPath.curve(to: NSPoint(x: 8.38, y: 16.5), controlPoint1: NSPoint(x: 11.5, y: 17.88), controlPoint2: NSPoint(x: 10.12, y: 16.5)) + bezierPath.close() + black.setFill() + bezierPath.fill() + + + //// Bezier 2 Drawing + let bezier2Path = NSBezierPath() + bezier2Path.move(to: NSPoint(x: 17.75, y: 15.25)) + bezier2Path.line(to: NSPoint(x: 12.75, y: 10.25)) + bezier2Path.line(to: NSPoint(x: 15.25, y: 7.75)) + bezier2Path.line(to: NSPoint(x: 15.25, y: 7.75)) + bezier2Path.line(to: NSPoint(x: 17.75, y: 7.75)) + bezier2Path.line(to: NSPoint(x: 17.75, y: 5.25)) + bezier2Path.line(to: NSPoint(x: 17.75, y: 5.25)) + bezier2Path.line(to: NSPoint(x: 20.25, y: 5.25)) + bezier2Path.line(to: NSPoint(x: 20.25, y: 2.75)) + bezier2Path.line(to: NSPoint(x: 21.12, y: 1.87)) + bezier2Path.curve(to: NSPoint(x: 22, y: 1.5), controlPoint1: NSPoint(x: 21.38, y: 1.62), controlPoint2: NSPoint(x: 21.62, y: 1.5)) + bezier2Path.line(to: NSPoint(x: 25.25, y: 1.5)) + bezier2Path.curve(to: NSPoint(x: 26.5, y: 2.75), controlPoint1: NSPoint(x: 26, y: 1.5), controlPoint2: NSPoint(x: 26.5, y: 2)) + bezier2Path.line(to: NSPoint(x: 26.5, y: 6)) + bezier2Path.curve(to: NSPoint(x: 26.12, y: 6.87), controlPoint1: NSPoint(x: 26.5, y: 6.37), controlPoint2: NSPoint(x: 26.38, y: 6.62)) + bezier2Path.line(to: NSPoint(x: 17.75, y: 15.25)) + bezier2Path.close() + black.setFill() + bezier2Path.fill() + + NSGraphicsContext.restoreGraphicsState() + + } + + @objc dynamic public class func drawActivityNewKey() { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Color Declarations + let setupKeyFill = NSColor(red: 0, green: 0, blue: 0, alpha: 1) + + //// Bezier Drawing + NSGraphicsContext.saveGraphicsState() + context.translateBy(x: 34.82, y: 34.64) + context.scaleBy(x: 2.3, y: 2.3) + + let bezierPath = NSBezierPath() + bezierPath.move(to: NSPoint(x: -3.03, y: 1)) + bezierPath.curve(to: NSPoint(x: -3.63, y: 0.35), controlPoint1: NSPoint(x: -3.26, y: 0.81), controlPoint2: NSPoint(x: -3.46, y: 0.59)) + bezierPath.curve(to: NSPoint(x: -9.05, y: -10.04), controlPoint1: NSPoint(x: -5.43, y: -3.12), controlPoint2: NSPoint(x: -7.24, y: -6.58)) + bezierPath.curve(to: NSPoint(x: -10.59, y: -10.09), controlPoint1: NSPoint(x: -9.62, y: -10), controlPoint2: NSPoint(x: -10.15, y: -9.95)) + bezierPath.curve(to: NSPoint(x: -11.29, y: -7.54), controlPoint1: NSPoint(x: -11.17, y: -9.17), controlPoint2: NSPoint(x: -11.29, y: -8.34)) + bezierPath.curve(to: NSPoint(x: -10.64, y: -7.33), controlPoint1: NSPoint(x: -11.07, y: -7.47), controlPoint2: NSPoint(x: -10.85, y: -7.4)) + bezierPath.curve(to: NSPoint(x: -10.2, y: -6.63), controlPoint1: NSPoint(x: -10.44, y: -7.14), controlPoint2: NSPoint(x: -10.3, y: -6.9)) + bezierPath.curve(to: NSPoint(x: -10.29, y: -5.99), controlPoint1: NSPoint(x: -10.23, y: -6.42), controlPoint2: NSPoint(x: -10.26, y: -6.21)) + bezierPath.curve(to: NSPoint(x: -9.4, y: -5.61), controlPoint1: NSPoint(x: -9.99, y: -5.86), controlPoint2: NSPoint(x: -9.7, y: -5.74)) + bezierPath.curve(to: NSPoint(x: -9.04, y: -5.03), controlPoint1: NSPoint(x: -9.25, y: -5.5), controlPoint2: NSPoint(x: -9.14, y: -5.3)) + bezierPath.curve(to: NSPoint(x: -9.05, y: -4.33), controlPoint1: NSPoint(x: -9.04, y: -4.8), controlPoint2: NSPoint(x: -9.05, y: -4.57)) + bezierPath.curve(to: NSPoint(x: -8.22, y: -4.03), controlPoint1: NSPoint(x: -8.77, y: -4.23), controlPoint2: NSPoint(x: -8.5, y: -4.13)) + bezierPath.curve(to: NSPoint(x: -7.85, y: -3.34), controlPoint1: NSPoint(x: -8.04, y: -3.84), controlPoint2: NSPoint(x: -7.92, y: -3.61)) + bezierPath.curve(to: NSPoint(x: -7.89, y: -2.04), controlPoint1: NSPoint(x: -7.86, y: -2.91), controlPoint2: NSPoint(x: -8.43, y: -2.27)) + bezierPath.curve(to: NSPoint(x: -7.46, y: -1.32), controlPoint1: NSPoint(x: -7.74, y: -1.8), controlPoint2: NSPoint(x: -7.6, y: -1.56)) + bezierPath.curve(to: NSPoint(x: -7.73, y: -0.4), controlPoint1: NSPoint(x: -7.55, y: -1.01), controlPoint2: NSPoint(x: -7.77, y: -0.48)) + bezierPath.curve(to: NSPoint(x: -6.87, y: 1.26), controlPoint1: NSPoint(x: -7.5, y: 0.15), controlPoint2: NSPoint(x: -7.1, y: 0.61)) + bezierPath.curve(to: NSPoint(x: -7.56, y: 2.15), controlPoint1: NSPoint(x: -7.09, y: 1.77), controlPoint2: NSPoint(x: -7.64, y: 1.66)) + bezierPath.curve(to: NSPoint(x: -6.31, y: 3.61), controlPoint1: NSPoint(x: -7.58, y: 2.76), controlPoint2: NSPoint(x: -6.64, y: 2.75)) + bezierPath.curve(to: NSPoint(x: -6.69, y: 9.16), controlPoint1: NSPoint(x: -7.44, y: 5.18), controlPoint2: NSPoint(x: -7.65, y: 7.32)) + bezierPath.curve(to: NSPoint(x: -1.41, y: 11.99), controlPoint1: NSPoint(x: -5.65, y: 11.16), controlPoint2: NSPoint(x: -3.51, y: 12.22)) + bezierPath.curve(to: NSPoint(x: -0.42, y: 12.08), controlPoint1: NSPoint(x: -1.09, y: 12.05), controlPoint2: NSPoint(x: -0.76, y: 12.08)) + bezierPath.curve(to: NSPoint(x: 0.64, y: 11.97), controlPoint1: NSPoint(x: -0.06, y: 12.08), controlPoint2: NSPoint(x: 0.3, y: 12.04)) + bezierPath.curve(to: NSPoint(x: 6.08, y: 9.2), controlPoint1: NSPoint(x: 2.78, y: 12.29), controlPoint2: NSPoint(x: 5, y: 11.24)) + bezierPath.curve(to: NSPoint(x: 5.71, y: 3.62), controlPoint1: NSPoint(x: 7.06, y: 7.35), controlPoint2: NSPoint(x: 6.85, y: 5.2)) + bezierPath.curve(to: NSPoint(x: 5.8, y: 1.82), controlPoint1: NSPoint(x: 5.53, y: 2.99), controlPoint2: NSPoint(x: 5.58, y: 2.38)) + bezierPath.curve(to: NSPoint(x: 11.29, y: -8.54), controlPoint1: NSPoint(x: 7.63, y: -1.63), controlPoint2: NSPoint(x: 9.46, y: -5.09)) + bezierPath.curve(to: NSPoint(x: 10.45, y: -9.84), controlPoint1: NSPoint(x: 10.93, y: -8.98), controlPoint2: NSPoint(x: 10.59, y: -9.4)) + bezierPath.curve(to: NSPoint(x: 7.95, y: -8.96), controlPoint1: NSPoint(x: 9.36, y: -9.79), controlPoint2: NSPoint(x: 8.61, y: -9.42)) + bezierPath.curve(to: NSPoint(x: 8.15, y: -8.31), controlPoint1: NSPoint(x: 8.02, y: -8.75), controlPoint2: NSPoint(x: 8.09, y: -8.53)) + bezierPath.curve(to: NSPoint(x: 7.82, y: -7.56), controlPoint1: NSPoint(x: 8.11, y: -8.04), controlPoint2: NSPoint(x: 7.99, y: -7.79)) + bezierPath.curve(to: NSPoint(x: 7.25, y: -7.26), controlPoint1: NSPoint(x: 7.63, y: -7.46), controlPoint2: NSPoint(x: 7.44, y: -7.36)) + bezierPath.curve(to: NSPoint(x: 7.43, y: -6.32), controlPoint1: NSPoint(x: 7.31, y: -6.95), controlPoint2: NSPoint(x: 7.37, y: -6.63)) + bezierPath.curve(to: NSPoint(x: 7.16, y: -5.69), controlPoint1: NSPoint(x: 7.43, y: -6.13), controlPoint2: NSPoint(x: 7.33, y: -5.92)) + bezierPath.curve(to: NSPoint(x: 6.59, y: -5.3), controlPoint1: NSPoint(x: 6.97, y: -5.56), controlPoint2: NSPoint(x: 6.78, y: -5.43)) + bezierPath.curve(to: NSPoint(x: 6.8, y: -4.45), controlPoint1: NSPoint(x: 6.66, y: -5.02), controlPoint2: NSPoint(x: 6.73, y: -4.73)) + bezierPath.curve(to: NSPoint(x: 6.45, y: -3.75), controlPoint1: NSPoint(x: 6.75, y: -4.19), controlPoint2: NSPoint(x: 6.63, y: -3.96)) + bezierPath.curve(to: NSPoint(x: 5.36, y: -3.04), controlPoint1: NSPoint(x: 6.08, y: -3.51), controlPoint2: NSPoint(x: 5.23, y: -3.62)) + bezierPath.curve(to: NSPoint(x: 5.01, y: -2.29), controlPoint1: NSPoint(x: 5.24, y: -2.79), controlPoint2: NSPoint(x: 5.12, y: -2.54)) + bezierPath.curve(to: NSPoint(x: 4.09, y: -1.98), controlPoint1: NSPoint(x: 4.7, y: -2.18), controlPoint2: NSPoint(x: 4.14, y: -2.07)) + bezierPath.curve(to: NSPoint(x: 3.22, y: -0.34), controlPoint1: NSPoint(x: 3.77, y: -1.48), controlPoint2: NSPoint(x: 3.62, y: -0.9)) + bezierPath.curve(to: NSPoint(x: 2.09, y: -0.4), controlPoint1: NSPoint(x: 2.67, y: -0.23), controlPoint2: NSPoint(x: 2.44, y: -0.74)) + bezierPath.curve(to: NSPoint(x: 1.59, y: 1.46), controlPoint1: NSPoint(x: 1.57, y: -0.07), controlPoint2: NSPoint(x: 2.12, y: 0.7)) + bezierPath.curve(to: NSPoint(x: 1.47, y: 1.46), controlPoint1: NSPoint(x: 1.55, y: 1.46), controlPoint2: NSPoint(x: 1.51, y: 1.46)) + bezierPath.curve(to: NSPoint(x: 1.13, y: 0.39), controlPoint1: NSPoint(x: 1.28, y: 1.13), controlPoint2: NSPoint(x: 1.17, y: 0.77)) + bezierPath.curve(to: NSPoint(x: 1.13, y: -11.33), controlPoint1: NSPoint(x: 1.13, y: -3.52), controlPoint2: NSPoint(x: 1.13, y: -7.42)) + bezierPath.curve(to: NSPoint(x: -0.22, y: -12.08), controlPoint1: NSPoint(x: 0.6, y: -11.55), controlPoint2: NSPoint(x: 0.1, y: -11.76)) + bezierPath.curve(to: NSPoint(x: -2.02, y: -10.14), controlPoint1: NSPoint(x: -1.16, y: -11.53), controlPoint2: NSPoint(x: -1.65, y: -10.85)) + bezierPath.curve(to: NSPoint(x: -1.54, y: -9.66), controlPoint1: NSPoint(x: -1.86, y: -9.98), controlPoint2: NSPoint(x: -1.7, y: -9.82)) + bezierPath.curve(to: NSPoint(x: -1.48, y: -8.84), controlPoint1: NSPoint(x: -1.45, y: -9.39), controlPoint2: NSPoint(x: -1.43, y: -9.12)) + bezierPath.curve(to: NSPoint(x: -1.85, y: -8.31), controlPoint1: NSPoint(x: -1.6, y: -8.66), controlPoint2: NSPoint(x: -1.72, y: -8.49)) + bezierPath.curve(to: NSPoint(x: -1.24, y: -7.56), controlPoint1: NSPoint(x: -1.65, y: -8.06), controlPoint2: NSPoint(x: -1.44, y: -7.81)) + bezierPath.curve(to: NSPoint(x: -1.19, y: -6.88), controlPoint1: NSPoint(x: -1.15, y: -7.4), controlPoint2: NSPoint(x: -1.14, y: -7.16)) + bezierPath.curve(to: NSPoint(x: -1.51, y: -6.27), controlPoint1: NSPoint(x: -1.3, y: -6.68), controlPoint2: NSPoint(x: -1.41, y: -6.47)) + bezierPath.curve(to: NSPoint(x: -0.92, y: -5.62), controlPoint1: NSPoint(x: -1.32, y: -6.05), controlPoint2: NSPoint(x: -1.12, y: -5.83)) + bezierPath.curve(to: NSPoint(x: -0.91, y: -4.83), controlPoint1: NSPoint(x: -0.85, y: -5.36), controlPoint2: NSPoint(x: -0.85, y: -5.1)) + bezierPath.curve(to: NSPoint(x: -1.54, y: -3.69), controlPoint1: NSPoint(x: -1.12, y: -4.45), controlPoint2: NSPoint(x: -1.92, y: -4.15)) + bezierPath.curve(to: NSPoint(x: -1.5, y: -2.86), controlPoint1: NSPoint(x: -1.53, y: -3.42), controlPoint2: NSPoint(x: -1.51, y: -3.14)) + bezierPath.curve(to: NSPoint(x: -2.17, y: -2.16), controlPoint1: NSPoint(x: -1.72, y: -2.63), controlPoint2: NSPoint(x: -2.16, y: -2.26)) + bezierPath.curve(to: NSPoint(x: -2.17, y: -0.3), controlPoint1: NSPoint(x: -2.21, y: -1.58), controlPoint2: NSPoint(x: -2.07, y: -0.99)) + bezierPath.curve(to: NSPoint(x: -3.19, y: 0.17), controlPoint1: NSPoint(x: -2.6, y: 0.05), controlPoint2: NSPoint(x: -3.04, y: -0.3)) + bezierPath.curve(to: NSPoint(x: -3.03, y: 1), controlPoint1: NSPoint(x: -3.35, y: 0.45), controlPoint2: NSPoint(x: -3.2, y: 0.69)) + bezierPath.close() + bezierPath.move(to: NSPoint(x: 0.84, y: 10.22)) + bezierPath.curve(to: NSPoint(x: 2.38, y: 8.68), controlPoint1: NSPoint(x: 1.69, y: 10.22), controlPoint2: NSPoint(x: 2.38, y: 9.53)) + bezierPath.curve(to: NSPoint(x: 0.84, y: 7.15), controlPoint1: NSPoint(x: 2.38, y: 7.84), controlPoint2: NSPoint(x: 1.69, y: 7.15)) + bezierPath.curve(to: NSPoint(x: -0.69, y: 8.68), controlPoint1: NSPoint(x: -0, y: 7.15), controlPoint2: NSPoint(x: -0.69, y: 7.84)) + bezierPath.curve(to: NSPoint(x: 0.84, y: 10.22), controlPoint1: NSPoint(x: -0.69, y: 9.53), controlPoint2: NSPoint(x: -0, y: 10.22)) + bezierPath.close() + bezierPath.windingRule = .evenOdd + setupKeyFill.setFill() + bezierPath.fill() + + NSGraphicsContext.restoreGraphicsState() + } + + @objc dynamic public class func drawActivitySiri(frame targetFrame: NSRect = NSRect(x: 0, y: 0, width: 60, height: 60), resizing: ResizingBehavior = .aspectFit) { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Resize to Target Frame + NSGraphicsContext.saveGraphicsState() + let resizedFrame: NSRect = resizing.apply(rect: NSRect(x: 0, y: 0, width: 60, height: 60), target: targetFrame) + context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY) + context.scaleBy(x: resizedFrame.width / 60, y: resizedFrame.height / 60) + + + //// Color Declarations + let black = NSColor(red: 0, green: 0, blue: 0, alpha: 1) + + //// Microphone + //// Group + NSGraphicsContext.saveGraphicsState() + context.translateBy(x: 31.01, y: 32.52) + context.scaleBy(x: 0.1, y: 0.1) + + + + //// Rectangle 2 Drawing + let rectangle2Path = NSBezierPath(roundedRect: NSRect(x: -101.03, y: -255.2, width: 172.05, height: 26.7), xRadius: 13.35, yRadius: 13.35) + black.setFill() + rectangle2Path.fill() + + + //// Rectangle 3 Drawing + let rectangle3Path = NSBezierPath(roundedRect: NSRect(x: -27.36, y: -255.23, width: 24.7, height: 138.95), xRadius: 12.35, yRadius: 12.35) + black.setFill() + rectangle3Path.fill() + + + //// Rectangle 4 Drawing + let rectangle4Path = NSBezierPath(roundedRect: NSRect(x: -87.16, y: -81.42, width: 144.3, height: 276.65), xRadius: 72.15, yRadius: 72.15) + black.setFill() + rectangle4Path.fill() + + + //// Bezier 2 Drawing + let bezier2Path = NSBezierPath() + bezier2Path.move(to: NSPoint(x: -14.28, y: -135.52)) + bezier2Path.line(to: NSPoint(x: -15.72, y: -135.52)) + bezier2Path.curve(to: NSPoint(x: -103.66, y: -98.14), controlPoint1: NSPoint(x: -48.94, y: -135.52), controlPoint2: NSPoint(x: -80.17, y: -122.25)) + bezier2Path.curve(to: NSPoint(x: -140.08, y: -7.9), controlPoint1: NSPoint(x: -127.14, y: -74.04), controlPoint2: NSPoint(x: -140.08, y: -41.99)) + bezier2Path.curve(to: NSPoint(x: -126.81, y: 5.71), controlPoint1: NSPoint(x: -140.08, y: -0.38), controlPoint2: NSPoint(x: -134.14, y: 5.71)) + bezier2Path.curve(to: NSPoint(x: -113.54, y: -7.9), controlPoint1: NSPoint(x: -119.48, y: 5.71), controlPoint2: NSPoint(x: -113.54, y: -0.38)) + bezier2Path.curve(to: NSPoint(x: -84.89, y: -78.88), controlPoint1: NSPoint(x: -113.54, y: -34.72), controlPoint2: NSPoint(x: -103.36, y: -59.92)) + bezier2Path.curve(to: NSPoint(x: -15.72, y: -108.29), controlPoint1: NSPoint(x: -66.41, y: -97.84), controlPoint2: NSPoint(x: -41.85, y: -108.29)) + bezier2Path.line(to: NSPoint(x: -14.28, y: -108.29)) + bezier2Path.curve(to: NSPoint(x: 54.89, y: -78.88), controlPoint1: NSPoint(x: 11.85, y: -108.29), controlPoint2: NSPoint(x: 36.41, y: -97.84)) + bezier2Path.curve(to: NSPoint(x: 83.54, y: -7.9), controlPoint1: NSPoint(x: 73.36, y: -59.92), controlPoint2: NSPoint(x: 83.54, y: -34.72)) + bezier2Path.curve(to: NSPoint(x: 96.81, y: 5.71), controlPoint1: NSPoint(x: 83.54, y: -0.38), controlPoint2: NSPoint(x: 89.48, y: 5.71)) + bezier2Path.curve(to: NSPoint(x: 110.08, y: -7.9), controlPoint1: NSPoint(x: 104.14, y: 5.71), controlPoint2: NSPoint(x: 110.08, y: -0.38)) + bezier2Path.curve(to: NSPoint(x: 73.65, y: -98.14), controlPoint1: NSPoint(x: 110.08, y: -41.99), controlPoint2: NSPoint(x: 97.14, y: -74.04)) + bezier2Path.curve(to: NSPoint(x: -14.28, y: -135.52), controlPoint1: NSPoint(x: 50.17, y: -122.25), controlPoint2: NSPoint(x: 18.94, y: -135.52)) + bezier2Path.close() + black.setFill() + bezier2Path.fill() + + + + NSGraphicsContext.restoreGraphicsState() + + NSGraphicsContext.restoreGraphicsState() + + } + + @objc dynamic public class func drawSettingsReportIcon(frame targetFrame: NSRect = NSRect(x: 0, y: 0, width: 100, height: 100), resizing: ResizingBehavior = .aspectFit) { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Resize to Target Frame + NSGraphicsContext.saveGraphicsState() + let resizedFrame: NSRect = resizing.apply(rect: NSRect(x: 0, y: 0, width: 100, height: 100), target: targetFrame) + context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY) + context.scaleBy(x: resizedFrame.width / 100, y: resizedFrame.height / 100) + + + //// Color Declarations + let background = NSColor(red: 1, green: 0.666, blue: 0.206, alpha: 1) + let fillColor5 = NSColor(red: 1, green: 1, blue: 1, alpha: 1) + + //// Rectangle Drawing + let rectanglePath = NSBezierPath(roundedRect: NSRect(x: 0, y: 0, width: 100, height: 100), xRadius: 25, yRadius: 25) + background.setFill() + rectanglePath.fill() + + + //// Group 19 + //// Group 18 + NSGraphicsContext.saveGraphicsState() + context.translateBy(x: 13, y: 13) + context.scaleBy(x: 0.15, y: 0.15) + + + + //// Group + //// Group 2 + //// Bezier Drawing + let bezierPath = NSBezierPath() + bezierPath.move(to: NSPoint(x: 304, y: 512)) + bezierPath.line(to: NSPoint(x: 208, y: 512)) + bezierPath.curve(to: NSPoint(x: 61.06, y: 451.04), controlPoint1: NSPoint(x: 152.19, y: 512), controlPoint2: NSPoint(x: 99.97, y: 490.3)) + bezierPath.curve(to: NSPoint(x: 0, y: 304), controlPoint1: NSPoint(x: 21.7, y: 412.03), controlPoint2: NSPoint(x: 0, y: 359.81)) + bezierPath.curve(to: NSPoint(x: 192, y: 96.61), controlPoint1: NSPoint(x: 0, y: 194.69), controlPoint2: NSPoint(x: 84.74, y: 104.8)) + bezierPath.line(to: NSPoint(x: 192, y: 16)) + bezierPath.curve(to: NSPoint(x: 202.18, y: 1.09), controlPoint1: NSPoint(x: 192, y: 9.41), controlPoint2: NSPoint(x: 196.06, y: 3.49)) + bezierPath.curve(to: NSPoint(x: 208, y: 0), controlPoint1: NSPoint(x: 204.1, y: 0.35), controlPoint2: NSPoint(x: 206.05, y: 0)) + bezierPath.curve(to: NSPoint(x: 219.78, y: 5.18), controlPoint1: NSPoint(x: 212.42, y: 0), controlPoint2: NSPoint(x: 216.7, y: 1.82)) + bezierPath.line(to: NSPoint(x: 303.04, y: 96)) + bezierPath.line(to: NSPoint(x: 304, y: 96)) + bezierPath.curve(to: NSPoint(x: 450.94, y: 156.96), controlPoint1: NSPoint(x: 359.81, y: 96), controlPoint2: NSPoint(x: 412.03, y: 117.7)) + bezierPath.curve(to: NSPoint(x: 512, y: 304), controlPoint1: NSPoint(x: 490.3, y: 195.97), controlPoint2: NSPoint(x: 512, y: 248.19)) + bezierPath.curve(to: NSPoint(x: 304, y: 512), controlPoint1: NSPoint(x: 512, y: 418.69), controlPoint2: NSPoint(x: 418.69, y: 512)) + bezierPath.close() + fillColor5.setFill() + bezierPath.fill() + + + + + + + //// Group 3 + + + //// Group 4 + + + //// Group 5 + + + //// Group 6 + + + //// Group 7 + + + //// Group 8 + + + //// Group 9 + + + //// Group 10 + + + //// Group 11 + + + //// Group 12 + + + //// Group 13 + + + //// Group 14 + + + //// Group 15 + + + //// Group 16 + + + //// Group 17 + + + + NSGraphicsContext.restoreGraphicsState() + + + //// Text Drawing + let textRect = NSRect(x: 29, y: 38, width: 44, height: 43) + let textStyle = NSMutableParagraphStyle() + textStyle.alignment = .center + let textFontAttributes = [ + .font: NSFont(name: "ArialRoundedMTBold", size: 42)!, + .foregroundColor: background, + .paragraphStyle: textStyle, + ] as [NSAttributedString.Key: Any] + + "!\n".draw(in: textRect.offsetBy(dx: 0, dy: 11), withAttributes: textFontAttributes) + + NSGraphicsContext.restoreGraphicsState() + + } + + @objc dynamic public class func drawSettingsLogsIcon(frame targetFrame: NSRect = NSRect(x: 0, y: 0, width: 100, height: 100), resizing: ResizingBehavior = .aspectFit) { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Resize to Target Frame + NSGraphicsContext.saveGraphicsState() + let resizedFrame: NSRect = resizing.apply(rect: NSRect(x: 0, y: 0, width: 100, height: 100), target: targetFrame) + context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY) + context.scaleBy(x: resizedFrame.width / 100, y: resizedFrame.height / 100) + + + //// Color Declarations + let fillColor5 = NSColor(red: 1, green: 1, blue: 1, alpha: 1) + let color = NSColor(red: 0.52, green: 0.557, blue: 0.577, alpha: 1) + + //// Rectangle Drawing + let rectanglePath = NSBezierPath(roundedRect: NSRect(x: 0, y: 0, width: 100, height: 100), xRadius: 25, yRadius: 25) + color.setFill() + rectanglePath.fill() + + + //// Group 17 + NSGraphicsContext.saveGraphicsState() + context.translateBy(x: 13, y: 87) + context.scaleBy(x: 0.15, y: 0.15) + + + + //// Group + //// Bezier Drawing + let bezierPath = NSBezierPath() + bezierPath.move(to: NSPoint(x: 437.31, y: -57.96)) + bezierPath.line(to: NSPoint(x: 422.34, y: -57.96)) + bezierPath.line(to: NSPoint(x: 422.34, y: -425.76)) + bezierPath.curve(to: NSPoint(x: 378.01, y: -470.08), controlPoint1: NSPoint(x: 422.34, y: -450.19), controlPoint2: NSPoint(x: 402.45, y: -470.08)) + bezierPath.line(to: NSPoint(x: 226.33, y: -470.08)) + bezierPath.line(to: NSPoint(x: 105.7, y: -470.08)) + bezierPath.line(to: NSPoint(x: 105.7, y: -483.72)) + bezierPath.curve(to: NSPoint(x: 133.99, y: -512), controlPoint1: NSPoint(x: 105.7, y: -499.33), controlPoint2: NSPoint(x: 118.38, y: -512)) + bezierPath.line(to: NSPoint(x: 285.62, y: -512)) + bezierPath.line(to: NSPoint(x: 437.25, y: -512)) + bezierPath.curve(to: NSPoint(x: 465.54, y: -483.72), controlPoint1: NSPoint(x: 452.87, y: -512), controlPoint2: NSPoint(x: 465.54, y: -499.33)) + bezierPath.line(to: NSPoint(x: 465.54, y: -86.24)) + bezierPath.curve(to: NSPoint(x: 437.31, y: -57.96), controlPoint1: NSPoint(x: 465.54, y: -70.63), controlPoint2: NSPoint(x: 452.92, y: -57.96)) + bezierPath.close() + fillColor5.setFill() + bezierPath.fill() + + + //// Bezier 2 Drawing + let bezier2Path = NSBezierPath() + bezier2Path.move(to: NSPoint(x: 226.33, y: -454.04)) + bezier2Path.line(to: NSPoint(x: 377.96, y: -454.04)) + bezier2Path.curve(to: NSPoint(x: 406.24, y: -425.76), controlPoint1: NSPoint(x: 393.57, y: -454.04), controlPoint2: NSPoint(x: 406.24, y: -441.37)) + bezier2Path.line(to: NSPoint(x: 406.24, y: -57.96)) + bezier2Path.line(to: NSPoint(x: 406.24, y: -28.28)) + bezier2Path.curve(to: NSPoint(x: 377.96, y: 0), controlPoint1: NSPoint(x: 406.24, y: -12.67), controlPoint2: NSPoint(x: 393.57, y: 0)) + bezier2Path.line(to: NSPoint(x: 226.33, y: 0)) + bezier2Path.line(to: NSPoint(x: 175.91, y: 0)) + bezier2Path.line(to: NSPoint(x: 175.91, y: -9.36)) + bezier2Path.curve(to: NSPoint(x: 176.07, y: -12.35), controlPoint1: NSPoint(x: 176.01, y: -10.32), controlPoint2: NSPoint(x: 176.07, y: -11.34)) + bezier2Path.line(to: NSPoint(x: 176.07, y: -74)) + bezier2Path.line(to: NSPoint(x: 176.07, y: -89.82)) + bezier2Path.curve(to: NSPoint(x: 136.23, y: -129.66), controlPoint1: NSPoint(x: 176.07, y: -111.8), controlPoint2: NSPoint(x: 158.21, y: -129.66)) + bezier2Path.line(to: NSPoint(x: 120.41, y: -129.66)) + bezier2Path.line(to: NSPoint(x: 58.76, y: -129.66)) + bezier2Path.curve(to: NSPoint(x: 56.3, y: -129.55), controlPoint1: NSPoint(x: 57.96, y: -129.66), controlPoint2: NSPoint(x: 57.1, y: -129.6)) + bezier2Path.line(to: NSPoint(x: 46.46, y: -129.55)) + bezier2Path.line(to: NSPoint(x: 46.46, y: -425.76)) + bezier2Path.curve(to: NSPoint(x: 74.75, y: -454.04), controlPoint1: NSPoint(x: 46.46, y: -441.37), controlPoint2: NSPoint(x: 59.13, y: -454.04)) + bezier2Path.line(to: NSPoint(x: 105.76, y: -454.04)) + bezier2Path.line(to: NSPoint(x: 226.33, y: -454.04)) + bezier2Path.line(to: NSPoint(x: 226.33, y: -454.04)) + bezier2Path.close() + bezier2Path.move(to: NSPoint(x: 232.05, y: -357.91)) + bezier2Path.line(to: NSPoint(x: 133.19, y: -357.91)) + bezier2Path.curve(to: NSPoint(x: 119.82, y: -344.54), controlPoint1: NSPoint(x: 125.81, y: -357.91), controlPoint2: NSPoint(x: 119.82, y: -351.92)) + bezier2Path.curve(to: NSPoint(x: 133.19, y: -331.17), controlPoint1: NSPoint(x: 119.82, y: -337.16), controlPoint2: NSPoint(x: 125.81, y: -331.17)) + bezier2Path.line(to: NSPoint(x: 232.1, y: -331.17)) + bezier2Path.curve(to: NSPoint(x: 245.47, y: -344.54), controlPoint1: NSPoint(x: 239.48, y: -331.17), controlPoint2: NSPoint(x: 245.47, y: -337.16)) + bezier2Path.curve(to: NSPoint(x: 232.05, y: -357.91), controlPoint1: NSPoint(x: 245.47, y: -351.92), controlPoint2: NSPoint(x: 239.43, y: -357.91)) + bezier2Path.close() + bezier2Path.move(to: NSPoint(x: 330.96, y: -289.68)) + bezier2Path.line(to: NSPoint(x: 133.19, y: -289.68)) + bezier2Path.curve(to: NSPoint(x: 119.82, y: -276.32), controlPoint1: NSPoint(x: 125.81, y: -289.68), controlPoint2: NSPoint(x: 119.82, y: -283.7)) + bezier2Path.curve(to: NSPoint(x: 133.19, y: -262.95), controlPoint1: NSPoint(x: 119.82, y: -268.94), controlPoint2: NSPoint(x: 125.81, y: -262.95)) + bezier2Path.line(to: NSPoint(x: 330.96, y: -262.95)) + bezier2Path.curve(to: NSPoint(x: 344.33, y: -276.32), controlPoint1: NSPoint(x: 338.34, y: -262.95), controlPoint2: NSPoint(x: 344.33, y: -268.94)) + bezier2Path.curve(to: NSPoint(x: 330.96, y: -289.68), controlPoint1: NSPoint(x: 344.33, y: -283.7), controlPoint2: NSPoint(x: 338.34, y: -289.68)) + bezier2Path.close() + bezier2Path.move(to: NSPoint(x: 133.19, y: -190.5)) + bezier2Path.line(to: NSPoint(x: 330.96, y: -190.5)) + bezier2Path.curve(to: NSPoint(x: 344.33, y: -203.87), controlPoint1: NSPoint(x: 338.34, y: -190.5), controlPoint2: NSPoint(x: 344.33, y: -196.49)) + bezier2Path.curve(to: NSPoint(x: 330.96, y: -217.24), controlPoint1: NSPoint(x: 344.33, y: -211.25), controlPoint2: NSPoint(x: 338.34, y: -217.24)) + bezier2Path.line(to: NSPoint(x: 133.19, y: -217.24)) + bezier2Path.curve(to: NSPoint(x: 119.82, y: -203.87), controlPoint1: NSPoint(x: 125.81, y: -217.24), controlPoint2: NSPoint(x: 119.82, y: -211.25)) + bezier2Path.curve(to: NSPoint(x: 133.19, y: -190.5), controlPoint1: NSPoint(x: 119.82, y: -196.49), controlPoint2: NSPoint(x: 125.81, y: -190.5)) + bezier2Path.close() + fillColor5.setFill() + bezier2Path.fill() + + + //// Bezier 3 Drawing + let bezier3Path = NSBezierPath() + bezier3Path.move(to: NSPoint(x: 58.76, y: -113.62)) + bezier3Path.line(to: NSPoint(x: 136.23, y: -113.62)) + bezier3Path.curve(to: NSPoint(x: 136.45, y: -113.62), controlPoint1: NSPoint(x: 136.29, y: -113.62), controlPoint2: NSPoint(x: 136.39, y: -113.62)) + bezier3Path.curve(to: NSPoint(x: 159.97, y: -90.09), controlPoint1: NSPoint(x: 149.39, y: -113.51), controlPoint2: NSPoint(x: 159.87, y: -103.03)) + bezier3Path.curve(to: NSPoint(x: 159.97, y: -89.88), controlPoint1: NSPoint(x: 159.97, y: -90.04), controlPoint2: NSPoint(x: 159.97, y: -89.93)) + bezier3Path.line(to: NSPoint(x: 159.97, y: -12.35)) + bezier3Path.curve(to: NSPoint(x: 148, y: -0.43), controlPoint1: NSPoint(x: 159.97, y: -5.19), controlPoint2: NSPoint(x: 154.09, y: -0.43)) + bezier3Path.curve(to: NSPoint(x: 139.66, y: -3.96), controlPoint1: NSPoint(x: 145.06, y: -0.43), controlPoint2: NSPoint(x: 142.12, y: -1.5)) + bezier3Path.line(to: NSPoint(x: 50.31, y: -93.3)) + bezier3Path.curve(to: NSPoint(x: 58.76, y: -113.62), controlPoint1: NSPoint(x: 42.83, y: -100.79), controlPoint2: NSPoint(x: 48.12, y: -113.62)) + bezier3Path.close() + fillColor5.setFill() + bezier3Path.fill() + + + + + //// Group 2 + + + //// Group 3 + + + //// Group 4 + + + //// Group 5 + + + //// Group 6 + + + //// Group 7 + + + //// Group 8 + + + //// Group 9 + + + //// Group 10 + + + //// Group 11 + + + //// Group 12 + + + //// Group 13 + + + //// Group 14 + + + //// Group 15 + + + //// Group 16 + + + + NSGraphicsContext.restoreGraphicsState() + + NSGraphicsContext.restoreGraphicsState() + + } + + @objc dynamic public class func drawSettingsBluetoothIcon(frame targetFrame: NSRect = NSRect(x: 0, y: 0, width: 100, height: 100), resizing: ResizingBehavior = .aspectFit) { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Resize to Target Frame + NSGraphicsContext.saveGraphicsState() + let resizedFrame: NSRect = resizing.apply(rect: NSRect(x: 0, y: 0, width: 100, height: 100), target: targetFrame) + context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY) + context.scaleBy(x: resizedFrame.width / 100, y: resizedFrame.height / 100) + + + //// Color Declarations + let fillColor5 = NSColor(red: 1, green: 1, blue: 1, alpha: 1) + + //// Rectangle Drawing + let rectanglePath = NSBezierPath(roundedRect: NSRect(x: 0, y: 0, width: 100, height: 100), xRadius: 25, yRadius: 25) + StyleKit.bluetoothBlue.setFill() + rectanglePath.fill() + + + //// Shape Drawing + NSGraphicsContext.saveGraphicsState() + context.translateBy(x: 23, y: 12.49) + context.scaleBy(x: 1.5, y: 1.5) + + let shapePath = NSBezierPath() + shapePath.move(to: NSPoint(x: 20, y: 37.91)) + shapePath.line(to: NSPoint(x: 24.05, y: 33.74)) + shapePath.line(to: NSPoint(x: 20.01, y: 29.58)) + shapePath.line(to: NSPoint(x: 20, y: 37.91)) + shapePath.line(to: NSPoint(x: 20, y: 37.91)) + shapePath.close() + shapePath.move(to: NSPoint(x: 20, y: 12.43)) + shapePath.line(to: NSPoint(x: 24.05, y: 16.6)) + shapePath.line(to: NSPoint(x: 20.01, y: 20.75)) + shapePath.line(to: NSPoint(x: 20, y: 12.43)) + shapePath.line(to: NSPoint(x: 20, y: 12.43)) + shapePath.close() + shapePath.move(to: NSPoint(x: 15.69, y: 25.16)) + shapePath.line(to: NSPoint(x: 6.96, y: 34.19)) + shapePath.line(to: NSPoint(x: 9.49, y: 36.79)) + shapePath.line(to: NSPoint(x: 16.45, y: 29.63)) + shapePath.line(to: NSPoint(x: 16.45, y: 46.81)) + shapePath.line(to: NSPoint(x: 29.1, y: 33.77)) + shapePath.line(to: NSPoint(x: 20.74, y: 25.16)) + shapePath.line(to: NSPoint(x: 29.1, y: 16.57)) + shapePath.line(to: NSPoint(x: 16.45, y: 3.53)) + shapePath.line(to: NSPoint(x: 16.45, y: 20.71)) + shapePath.line(to: NSPoint(x: 9.49, y: 13.55)) + shapePath.line(to: NSPoint(x: 6.96, y: 16.15)) + shapePath.line(to: NSPoint(x: 15.69, y: 25.16)) + shapePath.line(to: NSPoint(x: 15.69, y: 25.16)) + shapePath.close() + shapePath.move(to: NSPoint(x: 18.03, y: 0)) + shapePath.curve(to: NSPoint(x: 36.06, y: 25.17), controlPoint1: NSPoint(x: 28.7, y: 0), controlPoint2: NSPoint(x: 36.06, y: 5.22)) + shapePath.curve(to: NSPoint(x: 18.03, y: 50.34), controlPoint1: NSPoint(x: 36.06, y: 45.12), controlPoint2: NSPoint(x: 28.7, y: 50.34)) + shapePath.curve(to: NSPoint(x: 0, y: 25.17), controlPoint1: NSPoint(x: 7.36, y: 50.34), controlPoint2: NSPoint(x: 0, y: 45.12)) + shapePath.curve(to: NSPoint(x: 18.03, y: 0), controlPoint1: NSPoint(x: 0, y: 5.22), controlPoint2: NSPoint(x: 7.36, y: 0)) + shapePath.line(to: NSPoint(x: 18.03, y: 0)) + shapePath.line(to: NSPoint(x: 18.03, y: 0)) + shapePath.close() + shapePath.windingRule = .evenOdd + fillColor5.setFill() + shapePath.fill() + + NSGraphicsContext.restoreGraphicsState() + + NSGraphicsContext.restoreGraphicsState() + + } + + @objc dynamic public class func drawSettingsCloudIcon(frame targetFrame: NSRect = NSRect(x: 0, y: 0, width: 100, height: 100), resizing: ResizingBehavior = .aspectFit) { + //// General Declarations + let context = NSGraphicsContext.current!.cgContext + + //// Resize to Target Frame + NSGraphicsContext.saveGraphicsState() + let resizedFrame: NSRect = resizing.apply(rect: NSRect(x: 0, y: 0, width: 100, height: 100), target: targetFrame) + context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY) + context.scaleBy(x: resizedFrame.width / 100, y: resizedFrame.height / 100) + + + //// Color Declarations + let fillColor5 = NSColor(red: 1, green: 1, blue: 1, alpha: 1) + let fillColor3 = NSColor(red: 0.557, green: 0.757, blue: 0.937, alpha: 1) + + //// Rectangle Drawing + let rectanglePath = NSBezierPath(roundedRect: NSRect(x: 0, y: 0, width: 100, height: 100), xRadius: 25, yRadius: 25) + fillColor3.setFill() + rectanglePath.fill() + + + //// Page-1 + //// Group 3 + //// icloud.fill + //// shape + //// Cloud Drawing + let cloudPath = NSBezierPath() + cloudPath.move(to: NSPoint(x: 73.13, y: 23.81)) + cloudPath.line(to: NSPoint(x: 29.14, y: 23.81)) + cloudPath.curve(to: NSPoint(x: 10, y: 40.37), controlPoint1: NSPoint(x: 18.21, y: 23.81), controlPoint2: NSPoint(x: 10, y: 31.37)) + cloudPath.curve(to: NSPoint(x: 21.73, y: 55.12), controlPoint1: NSPoint(x: 10, y: 47.84), controlPoint2: NSPoint(x: 14.47, y: 54.02)) + cloudPath.curve(to: NSPoint(x: 38.31, y: 67.21), controlPoint1: NSPoint(x: 21.69, y: 63.69), controlPoint2: NSPoint(x: 30.45, y: 69.47)) + cloudPath.curve(to: NSPoint(x: 56.87, y: 77), controlPoint1: NSPoint(x: 42.05, y: 72.69), controlPoint2: NSPoint(x: 48.31, y: 77)) + cloudPath.curve(to: NSPoint(x: 80.19, y: 53.5), controlPoint1: NSPoint(x: 70.03, y: 77), controlPoint2: NSPoint(x: 80.26, y: 67.15)) + cloudPath.curve(to: NSPoint(x: 90.32, y: 38.99), controlPoint1: NSPoint(x: 86.45, y: 50.99), controlPoint2: NSPoint(x: 90.32, y: 45.39)) + cloudPath.curve(to: NSPoint(x: 73.13, y: 23.81), controlPoint1: NSPoint(x: 90.32, y: 30.58), controlPoint2: NSPoint(x: 82.78, y: 23.78)) + cloudPath.close() + fillColor5.setFill() + cloudPath.fill() + + NSGraphicsContext.restoreGraphicsState() + + } + + //// Generated Images + + @objc dynamic public class func imageOfSetupLock(imageSize: NSSize = NSSize(width: 230, height: 230), setupLockGear: NSColor = NSColor(red: 0.425, green: 0.46, blue: 0.499, alpha: 1)) -> NSImage { + return NSImage(size: imageSize, flipped: false) { _ in + StyleKit.drawSetupLock(frame: NSRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height), setupLockGear: setupLockGear) + + return true + } + } + + @objc dynamic public class func imageOfSetupLockSelected(imageSize: NSSize = NSSize(width: 230, height: 230), setupLockGear: NSColor = NSColor(red: 0.425, green: 0.46, blue: 0.499, alpha: 1)) -> NSImage { + return NSImage(size: imageSize, flipped: false) { _ in + StyleKit.drawSetupLockSelected(frame: NSRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height), setupLockGear: setupLockGear) + + return true + } + } + + @objc dynamic public class func imageOfSetupKey(imageSize: NSSize = NSSize(width: 230, height: 230)) -> NSImage { + return NSImage(size: imageSize, flipped: false) { _ in + StyleKit.drawSetupKey(frame: NSRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height)) + + return true + } + } + + @objc dynamic public class func imageOfSetupKeySelected(imageSize: NSSize = NSSize(width: 230, height: 230)) -> NSImage { + return NSImage(size: imageSize, flipped: false) { _ in + StyleKit.drawSetupKeySelected(frame: NSRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height)) + + return true + } + } + + @objc dynamic public class func imageOfPermissionBadgeAnytime(imageSize: NSSize = NSSize(width: 51, height: 51)) -> NSImage { + return NSImage(size: imageSize, flipped: false) { _ in + StyleKit.drawPermissionBadgeAnytime(frame: NSRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height)) + + return true + } + } + + @objc dynamic public class var imageOfActivityNewKey: NSImage { + if Cache.imageOfActivityNewKey != nil { + return Cache.imageOfActivityNewKey! + } + + Cache.imageOfActivityNewKey = NSImage(size: NSSize(width: 70, height: 70), flipped: false) { _ in + StyleKit.drawActivityNewKey() + + return true + } + + return Cache.imageOfActivityNewKey! + } + + + + + @objc(StyleKitResizingBehavior) + public enum ResizingBehavior: Int { + case aspectFit /// The content is proportionally resized to fit into the target rectangle. + case aspectFill /// The content is proportionally resized to completely fill the target rectangle. + case stretch /// The content is stretched to match the entire target rectangle. + case center /// The content is centered in the target rectangle, but it is NOT resized. + + public func apply(rect: NSRect, target: NSRect) -> NSRect { + if rect == target || target == NSRect.zero { + return rect + } + + var scales = NSSize.zero + scales.width = abs(target.width / rect.width) + scales.height = abs(target.height / rect.height) + + switch self { + case .aspectFit: + scales.width = min(scales.width, scales.height) + scales.height = scales.width + case .aspectFill: + scales.width = max(scales.width, scales.height) + scales.height = scales.width + case .stretch: + break + case .center: + scales.width = 1 + scales.height = 1 + } + + var result = rect.standardized + result.size.width *= scales.width + result.size.height *= scales.height + result.origin.x = target.minX + (target.width - result.width) / 2 + result.origin.y = target.minY + (target.height - result.height) / 2 + return result + } + } +} +#endif diff --git a/Xcode/LockKit/View/AppKit/PermissionIconViewNSView.swift b/Xcode/LockKit/View/AppKit/PermissionIconViewNSView.swift new file mode 100644 index 00000000..63202c3f --- /dev/null +++ b/Xcode/LockKit/View/AppKit/PermissionIconViewNSView.swift @@ -0,0 +1,74 @@ +// +// PermissionIconViewNSView.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/18/22. +// + +#if canImport(AppKit) +import Foundation +import SwiftUI +import CoreLock +import AppKit + +extension PermissionIconView: NSViewRepresentable { + + public func makeNSView(context: Context) -> NSViewType { + return NSViewType(permission: permission) + } + + public func updateNSView(_ view: NSViewType, context: Context) { + view.permission = permission + } +} + +public extension PermissionIconView { + + @objc(LockPermissionIconView) + final class NSViewType: NSView { + + // MARK: - Properties + + public var permission: PermissionType = .admin { + didSet { setNeedsDisplay(bounds) } + } + + // MARK: - Initialization + + public init( + permission: PermissionType, + frame: CGRect = CGRect(origin: .zero, size: CGSize(width: 48, height: 48)) + ) { + self.permission = permission + super.init(frame: frame) + //self.backgroundColor = .clear + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func awakeFromNib() { + super.awakeFromNib() + //self.backgroundColor = .clear + } + + // MARK: - Methods + + public override func draw(_ rect: NSRect) { + + switch permission { + case .owner: + StyleKit.drawPermissionBadgeOwner(frame: bounds) + case .admin: + StyleKit.drawPermissionBadgeAdmin(frame: bounds) + case .anytime: + StyleKit.drawPermissionBadgeAnytime(frame: bounds) + case .scheduled: + StyleKit.drawPermissionBadgeScheduled(frame: bounds) + } + } + } +} + +#endif diff --git a/Xcode/LockKit/View/PermissionIconView.swift b/Xcode/LockKit/View/PermissionIconView.swift new file mode 100644 index 00000000..f773600a --- /dev/null +++ b/Xcode/LockKit/View/PermissionIconView.swift @@ -0,0 +1,32 @@ +// +// PermissionIconView.swift +// LockKit +// +// Created by Alsey Coleman Miller on 8/23/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import SwiftUI +import CoreLock + +/// Renders lock permission icon. +public struct PermissionIconView: View { + + @State + var permission: PermissionType = .admin +} + +// MARK: - Preview + +struct PermissionIconView_Previews: PreviewProvider { + static var previews: some View { + Group { + PermissionIconView() + .padding(5.0) + .preferredColorScheme(.light) + .previewLayout(.sizeThatFits) + .frame(width: 100, height: 100, alignment: .center) + } + } +} diff --git a/Xcode/LockKit/View/UIKit/PermissionIconViewUIView.swift b/Xcode/LockKit/View/UIKit/PermissionIconViewUIView.swift new file mode 100644 index 00000000..147df8f6 --- /dev/null +++ b/Xcode/LockKit/View/UIKit/PermissionIconViewUIView.swift @@ -0,0 +1,106 @@ +// +// PermissionIconViewUIView.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/18/22. +// + +#if canImport(UIKit) +import Foundation +import SwiftUI +import CoreLock +import UIKit + +extension PermissionIconView: UIViewRepresentable { + + public func makeUIView(context: Context) -> UIViewType { + return UIViewType(permission: permission) + } + + public func updateUIView(_ view: UIViewType, context: Context) { + view.permission = permission + } +} + +public extension PermissionIconView { + + @objc(LockPermissionIconView) + @IBDesignable + final class UIViewType: UIView { + + // MARK: - Properties + + public var permission: PermissionType = .admin { + didSet { setNeedsDisplay() } + } + + // MARK: - Initialization + + public init( + permission: PermissionType, + frame: CGRect = CGRect(origin: .zero, size: CGSize(width: 48, height: 48)) + ) { + self.permission = permission + super.init(frame: frame) + self.backgroundColor = .clear + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func awakeFromNib() { + super.awakeFromNib() + self.backgroundColor = .clear + } + + // MARK: - Methods + + public override func draw(_ rect: CGRect) { + + switch permission { + case .owner: + StyleKit.drawPermissionBadgeOwner(frame: bounds) + case .admin: + StyleKit.drawPermissionBadgeAdmin(frame: bounds) + case .anytime: + StyleKit.drawPermissionBadgeAnytime(frame: bounds) + case .scheduled: + StyleKit.drawPermissionBadgeScheduled(frame: bounds) + } + } + } +} + +// MARK: - IB Support + +public extension PermissionIconView.UIViewType { + + @IBInspectable + var permissionName: String { + get { return PermissionName(permission).rawValue } + set { if let name = PermissionName(rawValue: newValue) { self.permission = name.permission } } + } +} + +// MARK: - Supporting Types + +private extension PermissionIconView.UIViewType { + + enum PermissionName: String { + + case owner + case admin + case anytime + case scheduled + + init(_ permission: PermissionType) { + self = unsafeBitCast(permission, to: PermissionName.self) + } + + var permission: PermissionType { + return unsafeBitCast(self, to: PermissionType.self) + } + } +} +#endif diff --git a/iOS/LockKit/View/StyleKit.swift b/Xcode/LockKit/View/UIKit/UIStyleKit.swift similarity index 99% rename from iOS/LockKit/View/StyleKit.swift rename to Xcode/LockKit/View/UIKit/UIStyleKit.swift index de41f4cd..c8e3a899 100644 --- a/iOS/LockKit/View/StyleKit.swift +++ b/Xcode/LockKit/View/UIKit/UIStyleKit.swift @@ -10,7 +10,7 @@ // - +#if canImport(UIKit) import UIKit public class StyleKit : NSObject { @@ -2797,3 +2797,4 @@ public class StyleKit : NSObject { } } } +#endif diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 8e02dd61..d33da67a 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -10,16 +10,19 @@ 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276BB28D708A000AF171B /* CoreLock */; }; 6E3276C328D70A3700AF171B /* HomeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E3276C228D70A3700AF171B /* HomeKit.framework */; }; 6E3276C628D70A3700AF171B /* RequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276C528D70A3700AF171B /* RequestHandler.swift */; }; - 6E3276CA28D70A3700AF171B /* MatterLock.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6E3276C128D70A3700AF171B /* MatterLock.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 6E3276D028D70C9400AF171B /* NearbyDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276CF28D70C9400AF171B /* NearbyDevicesView.swift */; }; 6E3276D228D70CE100AF171B /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276D128D70CE100AF171B /* Store.swift */; }; 6E3276D728D70FA000AF171B /* DarwinGATT in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276D628D70FA000AF171B /* DarwinGATT */; }; 6E3276D928D70FA000AF171B /* GATT in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276D828D70FA000AF171B /* GATT */; }; 6E3276DC28D7195400AF171B /* Central.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276DB28D7195400AF171B /* Central.swift */; }; + 6E4CB61028D7867700116573 /* LockRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276E528D782B900AF171B /* LockRowView.swift */; }; + 6E4CB61A28D788AA00116573 /* UIStyleKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB61828D788AA00116573 /* UIStyleKit.swift */; }; + 6E4CB61B28D788AA00116573 /* PermissionIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB61928D788AA00116573 /* PermissionIconView.swift */; }; + 6E4CB61F28D7902600116573 /* NSStyleKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB61E28D7902600116573 /* NSStyleKit.swift */; }; + 6E4CB62128D7907200116573 /* PermissionIconViewUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62028D7907200116573 /* PermissionIconViewUIView.swift */; }; + 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62228D7907E00116573 /* PermissionIconViewNSView.swift */; }; + 6E4CB62528D792BF00116573 /* InfoPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CB62428D792BF00116573 /* InfoPlist.swift */; }; 6EA7768528D7061600018FA3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA7768428D7061600018FA3 /* App.swift */; }; - 6EA7768728D7061600018FA3 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA7768628D7061600018FA3 /* Persistence.swift */; }; - 6EA7768A28D7061600018FA3 /* SmartLock.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 6EA7768828D7061600018FA3 /* SmartLock.xcdatamodeld */; }; - 6EA7768C28D7061600018FA3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA7768B28D7061600018FA3 /* ContentView.swift */; }; 6EA7768E28D7061600018FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6EA7768D28D7061600018FA3 /* Assets.xcassets */; }; 6EA7769228D7061600018FA3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6EA7769128D7061600018FA3 /* Preview Assets.xcassets */; }; 6EA776A028D707FE00018FA3 /* LockKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 6EA7769F28D707FE00018FA3 /* LockKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -28,13 +31,6 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 6E3276C828D70A3700AF171B /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 6EA7767928D7061600018FA3 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 6E3276C028D70A3700AF171B; - remoteInfo = MatterLock; - }; 6EA776A128D707FE00018FA3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 6EA7767928D7061600018FA3 /* Project object */; @@ -45,17 +41,6 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ - 6E3276CE28D70A3700AF171B /* Embed Foundation Extensions */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 13; - files = ( - 6E3276CA28D70A3700AF171B /* MatterLock.appex in Embed Foundation Extensions */, - ); - name = "Embed Foundation Extensions"; - runOnlyForDeploymentPostprocessing = 0; - }; 6EA776A828D707FE00018FA3 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -78,11 +63,15 @@ 6E3276CF28D70C9400AF171B /* NearbyDevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyDevicesView.swift; sourceTree = ""; }; 6E3276D128D70CE100AF171B /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; 6E3276DB28D7195400AF171B /* Central.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Central.swift; sourceTree = ""; }; + 6E3276E528D782B900AF171B /* LockRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockRowView.swift; sourceTree = ""; }; + 6E4CB61828D788AA00116573 /* UIStyleKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIStyleKit.swift; sourceTree = ""; }; + 6E4CB61928D788AA00116573 /* PermissionIconView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PermissionIconView.swift; sourceTree = ""; }; + 6E4CB61E28D7902600116573 /* NSStyleKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSStyleKit.swift; sourceTree = ""; }; + 6E4CB62028D7907200116573 /* PermissionIconViewUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionIconViewUIView.swift; sourceTree = ""; }; + 6E4CB62228D7907E00116573 /* PermissionIconViewNSView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionIconViewNSView.swift; sourceTree = ""; }; + 6E4CB62428D792BF00116573 /* InfoPlist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InfoPlist.swift; path = ../../../iOS/SmartLock/InfoPlist.swift; sourceTree = ""; }; 6EA7768128D7061600018FA3 /* SmartLock.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SmartLock.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6EA7768428D7061600018FA3 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; - 6EA7768628D7061600018FA3 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; - 6EA7768928D7061600018FA3 /* SmartLock.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SmartLock.xcdatamodel; sourceTree = ""; }; - 6EA7768B28D7061600018FA3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 6EA7768D28D7061600018FA3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 6EA7768F28D7061600018FA3 /* SmartLock.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SmartLock.entitlements; sourceTree = ""; }; 6EA7769128D7061600018FA3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; @@ -140,7 +129,6 @@ 6E3276D328D70D6900AF171B /* View */ = { isa = PBXGroup; children = ( - 6EA7768B28D7061600018FA3 /* ContentView.swift */, 6E3276CF28D70C9400AF171B /* NearbyDevicesView.swift */, ); path = View; @@ -149,8 +137,7 @@ 6E3276D428D70D7500AF171B /* Model */ = { isa = PBXGroup; children = ( - 6EA7768828D7061600018FA3 /* SmartLock.xcdatamodeld */, - 6EA7768628D7061600018FA3 /* Persistence.swift */, + 6E4CB62428D792BF00116573 /* InfoPlist.swift */, ); path = Model; sourceTree = ""; @@ -164,6 +151,35 @@ path = Model; sourceTree = ""; }; + 6E4CB60F28D7866600116573 /* View */ = { + isa = PBXGroup; + children = ( + 6E4CB61D28D7901D00116573 /* AppKit */, + 6E4CB61C28D788B900116573 /* UIKit */, + 6E3276E528D782B900AF171B /* LockRowView.swift */, + 6E4CB61928D788AA00116573 /* PermissionIconView.swift */, + ); + path = View; + sourceTree = ""; + }; + 6E4CB61C28D788B900116573 /* UIKit */ = { + isa = PBXGroup; + children = ( + 6E4CB61828D788AA00116573 /* UIStyleKit.swift */, + 6E4CB62028D7907200116573 /* PermissionIconViewUIView.swift */, + ); + path = UIKit; + sourceTree = ""; + }; + 6E4CB61D28D7901D00116573 /* AppKit */ = { + isa = PBXGroup; + children = ( + 6E4CB61E28D7902600116573 /* NSStyleKit.swift */, + 6E4CB62228D7907E00116573 /* PermissionIconViewNSView.swift */, + ); + path = AppKit; + sourceTree = ""; + }; 6EA7767828D7061600018FA3 = { isa = PBXGroup; children = ( @@ -211,6 +227,7 @@ isa = PBXGroup; children = ( 6EA7769F28D707FE00018FA3 /* LockKit.h */, + 6E4CB60F28D7866600116573 /* View */, 6E3276DA28D7136400AF171B /* Model */, ); path = LockKit; @@ -263,13 +280,11 @@ 6EA7767E28D7061600018FA3 /* Frameworks */, 6EA7767F28D7061600018FA3 /* Resources */, 6EA776A828D707FE00018FA3 /* Embed Frameworks */, - 6E3276CE28D70A3700AF171B /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( 6EA776A228D707FE00018FA3 /* PBXTargetDependency */, - 6E3276C928D70A3700AF171B /* PBXTargetDependency */, ); name = SmartLock; productName = SmartLock; @@ -383,11 +398,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6E4CB62528D792BF00116573 /* InfoPlist.swift in Sources */, 6EA7768528D7061600018FA3 /* App.swift in Sources */, - 6EA7768A28D7061600018FA3 /* SmartLock.xcdatamodeld in Sources */, - 6EA7768C28D7061600018FA3 /* ContentView.swift in Sources */, 6E3276D028D70C9400AF171B /* NearbyDevicesView.swift in Sources */, - 6EA7768728D7061600018FA3 /* Persistence.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -395,19 +408,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6E4CB61F28D7902600116573 /* NSStyleKit.swift in Sources */, + 6E4CB61028D7867700116573 /* LockRowView.swift in Sources */, + 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */, 6E3276D228D70CE100AF171B /* Store.swift in Sources */, + 6E4CB61A28D788AA00116573 /* UIStyleKit.swift in Sources */, 6E3276DC28D7195400AF171B /* Central.swift in Sources */, + 6E4CB61B28D788AA00116573 /* PermissionIconView.swift in Sources */, + 6E4CB62128D7907200116573 /* PermissionIconViewUIView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 6E3276C928D70A3700AF171B /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 6E3276C028D70A3700AF171B /* MatterLock */; - targetProxy = 6E3276C828D70A3700AF171B /* PBXContainerItemProxy */; - }; 6EA776A228D707FE00018FA3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 6EA7769C28D707FE00018FA3 /* LockKit */; @@ -602,6 +616,8 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bluetooth is used to scan for nearby locks in the background."; + INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Bluetooth is needed to connect to your smart lock."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -639,6 +655,8 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bluetooth is used to scan for nearby locks in the background."; + INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Bluetooth is needed to connect to your smart lock."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -803,19 +821,6 @@ productName = GATT; }; /* End XCSwiftPackageProductDependency section */ - -/* Begin XCVersionGroup section */ - 6EA7768828D7061600018FA3 /* SmartLock.xcdatamodeld */ = { - isa = XCVersionGroup; - children = ( - 6EA7768928D7061600018FA3 /* SmartLock.xcdatamodel */, - ); - currentVersion = 6EA7768928D7061600018FA3 /* SmartLock.xcdatamodel */; - path = SmartLock.xcdatamodeld; - sourceTree = ""; - versionGroupType = wrapper.xcdatamodel; - }; -/* End XCVersionGroup section */ }; rootObject = 6EA7767928D7061600018FA3 /* Project object */; } diff --git a/Xcode/SmartLock/Model/Persistence.swift b/Xcode/SmartLock/Model/Persistence.swift deleted file mode 100644 index 0c2a6efb..00000000 --- a/Xcode/SmartLock/Model/Persistence.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// Persistence.swift -// SmartLock -// -// Created by Alsey Coleman Miller on 9/18/22. -// - -import CoreData - -struct PersistenceController { - - static let shared = PersistenceController() - - static var preview: PersistenceController = { - let result = PersistenceController(inMemory: true) - let viewContext = result.container.viewContext - for _ in 0..<10 { - let newItem = Item(context: viewContext) - newItem.timestamp = Date() - } - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - return result - }() - - let container: NSPersistentContainer - - init(inMemory: Bool = false) { - container = NSPersistentContainer(name: "SmartLock") - if inMemory { - container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") - } - container.loadPersistentStores(completionHandler: { (storeDescription, error) in - if let error = error as NSError? { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - - /* - Typical reasons for an error here include: - * The parent directory does not exist, cannot be created, or disallows writing. - * The persistent store is not accessible, due to permissions or data protection when the device is locked. - * The device is out of space. - * The store could not be migrated to the current model version. - Check the error message to determine what the actual problem was. - */ - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - container.viewContext.automaticallyMergesChangesFromParent = true - } -} diff --git a/Xcode/SmartLock/Model/SmartLock.xcdatamodeld/.xccurrentversion b/Xcode/SmartLock/Model/SmartLock.xcdatamodeld/.xccurrentversion index 0a618e66..0c67376e 100644 --- a/Xcode/SmartLock/Model/SmartLock.xcdatamodeld/.xccurrentversion +++ b/Xcode/SmartLock/Model/SmartLock.xcdatamodeld/.xccurrentversion @@ -1,8 +1,5 @@ - - _XCCurrentVersionName - SmartLock.xcdatamodel - + diff --git a/Xcode/SmartLock/View/ContentView.swift b/Xcode/SmartLock/View/ContentView.swift deleted file mode 100644 index 34f2b9e5..00000000 --- a/Xcode/SmartLock/View/ContentView.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// ContentView.swift -// SmartLock -// -// Created by Alsey Coleman Miller on 9/18/22. -// - -import SwiftUI -import CoreData - -struct ContentView: View { - @Environment(\.managedObjectContext) private var viewContext - - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)], - animation: .default) - private var items: FetchedResults - - var body: some View { - NavigationView { - List { - ForEach(items) { item in - NavigationLink { - Text("Item at \(item.timestamp!, formatter: itemFormatter)") - } label: { - Text(item.timestamp!, formatter: itemFormatter) - } - } - .onDelete(perform: deleteItems) - } - .toolbar { -#if os(iOS) - ToolbarItem(placement: .navigationBarTrailing) { - EditButton() - } -#endif - ToolbarItem { - Button(action: addItem) { - Label("Add Item", systemImage: "plus") - } - } - } - Text("Select an item") - } - } - - private func addItem() { - withAnimation { - let newItem = Item(context: viewContext) - newItem.timestamp = Date() - - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - } - - private func deleteItems(offsets: IndexSet) { - withAnimation { - offsets.map { items[$0] }.forEach(viewContext.delete) - - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - } -} - -private let itemFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .short - formatter.timeStyle = .medium - return formatter -}() - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) - } -} diff --git a/Xcode/SmartLock/View/NearbyDevicesView.swift b/Xcode/SmartLock/View/NearbyDevicesView.swift index 956597a2..6ef2b5cf 100644 --- a/Xcode/SmartLock/View/NearbyDevicesView.swift +++ b/Xcode/SmartLock/View/NearbyDevicesView.swift @@ -14,6 +14,27 @@ struct NearbyDevicesView: View { var store: Store = .shared var body: some View { + #if os(iOS) + list + .navigationBarTitle(Text("Nearby"), displayMode: .automatic) + .navigationBarItems(trailing: trailingButtonItem) + #elseif os(macOS) + list + .navigationTitle(Text("Nearby")) + #endif + } +} + +private extension NearbyDevicesView { + + var peripherals: [NativePeripheral] { + store.peripherals + .lazy + .sorted(by: { $0.value.rssi < $1.value.rssi }) + .map { $0.key } + } + + var list: some View { ScrollView { LazyVStack(alignment: .leading) { ForEach(peripherals, id: \.id) { @@ -30,15 +51,9 @@ struct NearbyDevicesView: View { } } } -} - -extension NearbyDevicesView { - var peripherals: [NativePeripheral] { - store.peripherals - .lazy - .sorted(by: { $0.value.rssi < $1.value.rssi }) - .map { $0.key } + var trailingButtonItem: some View { + EmptyView() } } diff --git a/iOS/LockKit/View/PermissionIconView.swift b/iOS/LockKit/View/PermissionIconView.swift deleted file mode 100644 index 5be26ccd..00000000 --- a/iOS/LockKit/View/PermissionIconView.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// PermissionIconView.swift -// LockKit -// -// Created by Alsey Coleman Miller on 8/23/19. -// Copyright © 2019 ColemanCDA. All rights reserved. -// - -import Foundation -import UIKit -import CoreLock - -@IBDesignable -public final class PermissionIconView: UIView { - - // MARK: - Properties - - public var permission: PermissionType = .admin { - didSet { setNeedsDisplay() } - } - - // MARK: - Initialization - - public override func awakeFromNib() { - super.awakeFromNib() - - self.backgroundColor = .clear - } - - // MARK: - Methods - - public override func draw(_ rect: CGRect) { - - switch permission { - case .owner: - StyleKit.drawPermissionBadgeOwner(frame: bounds) - case .admin: - StyleKit.drawPermissionBadgeAdmin(frame: bounds) - case .anytime: - StyleKit.drawPermissionBadgeAnytime(frame: bounds) - case .scheduled: - StyleKit.drawPermissionBadgeScheduled(frame: bounds) - } - } -} - -// MARK: - IB Support - -public extension PermissionIconView { - - @IBInspectable - var permissionName: String { - get { return PermissionName(permission).rawValue } - set { if let name = PermissionName(rawValue: newValue) { self.permission = name.permission } } - } -} - -// MARK: - Supporting Types - -private extension PermissionIconView { - - enum PermissionName: String { - - case owner - case admin - case anytime - case scheduled - - init(_ permission: PermissionType) { - self = unsafeBitCast(permission, to: PermissionName.self) - } - - var permission: PermissionType { - return unsafeBitCast(self, to: PermissionType.self) - } - } -} From 8f65d3fbc49e6942175574d60cff15d5b49a8d56 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 12:28:59 -0700 Subject: [PATCH 048/229] [App] Added `LockRowView` --- Xcode/LockKit/View/LockRowView.swift | 142 ++++++++++++++++++++ Xcode/LockKit/View/PermissionIconView.swift | 16 ++- Xcode/SmartLock.xcodeproj/project.pbxproj | 8 ++ 3 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 Xcode/LockKit/View/LockRowView.swift diff --git a/Xcode/LockKit/View/LockRowView.swift b/Xcode/LockKit/View/LockRowView.swift new file mode 100644 index 00000000..fffd8e2f --- /dev/null +++ b/Xcode/LockKit/View/LockRowView.swift @@ -0,0 +1,142 @@ +// +// LockCellView.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/18/22. +// + +import SwiftUI +import CoreLock + +public struct LockRowView: View { + + public let image: Image + + public let title: String + + public let subtitle: String? + + public var body: some View { + HStack(alignment: .center, spacing: 16) { + VStack { + ImageView(image: image) + .frame(width: 50, height: 50, alignment: .center) + } + VStack(alignment: .leading, spacing: 8) { + Text(verbatim: title) + .font(.system(size: 19)) + if let subtitle = subtitle { + Text(verbatim: subtitle) + .font(.system(size: 14)) + .foregroundColor(.gray) + } + } + } + } +} + +public extension LockRowView { + + enum Image { + case loading + case permission(PermissionType) + case emoji(Character) + case symbol(String) + #if canImport(UIKit) + case image(UIImage) + #elseif canImport(AppKit) + case image(NSImage) + #endif + } +} + +extension LockRowView { + + struct ImageView: View { + + let image: Image + + var body: some View { + switch image { + case .loading: + #if os(iOS) + AnyView(ActivityIndicatorView(style: .large)) + #else + AnyView( + ProgressView() + .progressViewStyle(.circular) + ) + #endif + case let .permission(permission): + AnyView( + PermissionIconView(permission: permission) + ) + case let .emoji(emoji): + AnyView( + Text(verbatim: String(emoji)) + .font(.system(size: 43)) + ) + case let .symbol(symbol): + AnyView( + SwiftUI.Image(systemName: symbol) + .font(.system(size: 40)) + ) + case let .image(image): + #if canImport(UIKit) + AnyView( + SwiftUI.Image(uiImage: image) + ) + #elseif canImport(AppKit) + AnyView( + SwiftUI.Image(nsImage: image) + ) + #endif + } + } + } +} + +// MARK: - Preview + +struct LockRowView_Previews: PreviewProvider { + static var previews: some View { + List { + LockRowView( + image: .loading, + title: "Loading...", + subtitle: nil + ) + LockRowView( + image: .permission(.admin), + title: "Lock Name", + subtitle: "Anytime" + ) + LockRowView( + image: .permission(.admin), + title: "Office door", + subtitle: "Admin" + ) + LockRowView( + image: .permission(.owner), + title: "My house", + subtitle: "Owner" + ) + LockRowView( + image: .permission(.anytime), + title: "Home", + subtitle: "Anytime" + ) + LockRowView( + image: .emoji("🔓"), + title: "Unlock", + subtitle: "By Alsey Coleman Miller" + ) + LockRowView( + image: .symbol("bonjour"), + title: "Bonjour", + subtitle: nil + ) + .symbolRenderingMode(.multicolor) + } + } +} diff --git a/Xcode/LockKit/View/PermissionIconView.swift b/Xcode/LockKit/View/PermissionIconView.swift index f773600a..fd72e753 100644 --- a/Xcode/LockKit/View/PermissionIconView.swift +++ b/Xcode/LockKit/View/PermissionIconView.swift @@ -13,19 +13,21 @@ import CoreLock /// Renders lock permission icon. public struct PermissionIconView: View { - @State - var permission: PermissionType = .admin + let permission: PermissionType } // MARK: - Preview struct PermissionIconView_Previews: PreviewProvider { static var previews: some View { - Group { - PermissionIconView() - .padding(5.0) - .preferredColorScheme(.light) - .previewLayout(.sizeThatFits) + VStack(spacing: 20) { + PermissionIconView(permission: .owner) + .frame(width: 100, height: 100, alignment: .center) + PermissionIconView(permission: .admin) + .frame(width: 100, height: 100, alignment: .center) + PermissionIconView(permission: .anytime) + .frame(width: 100, height: 100, alignment: .center) + PermissionIconView(permission: .scheduled) .frame(width: 100, height: 100, alignment: .center) } } diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index d33da67a..b9855f73 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -28,6 +28,8 @@ 6EA776A028D707FE00018FA3 /* LockKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 6EA7769F28D707FE00018FA3 /* LockKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6EA776A328D707FE00018FA3 /* LockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; }; 6EA776A428D707FE00018FA3 /* LockKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 6ED248CA28D79DDA00F78D07 /* LockEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ED248C928D79DDA00F78D07 /* LockEventView.swift */; }; + 6ED248CC28D7A3BF00F78D07 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ED248CB28D7A3BF00F78D07 /* ActivityIndicatorView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -77,6 +79,8 @@ 6EA7769128D7061600018FA3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 6EA7769D28D707FE00018FA3 /* LockKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LockKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6EA7769F28D707FE00018FA3 /* LockKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LockKit.h; sourceTree = ""; }; + 6ED248C928D79DDA00F78D07 /* LockEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockEventView.swift; sourceTree = ""; }; + 6ED248CB28D7A3BF00F78D07 /* ActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -157,6 +161,7 @@ 6E4CB61D28D7901D00116573 /* AppKit */, 6E4CB61C28D788B900116573 /* UIKit */, 6E3276E528D782B900AF171B /* LockRowView.swift */, + 6ED248C928D79DDA00F78D07 /* LockEventView.swift */, 6E4CB61928D788AA00116573 /* PermissionIconView.swift */, ); path = View; @@ -167,6 +172,7 @@ children = ( 6E4CB61828D788AA00116573 /* UIStyleKit.swift */, 6E4CB62028D7907200116573 /* PermissionIconViewUIView.swift */, + 6ED248CB28D7A3BF00F78D07 /* ActivityIndicatorView.swift */, ); path = UIKit; sourceTree = ""; @@ -409,6 +415,8 @@ buildActionMask = 2147483647; files = ( 6E4CB61F28D7902600116573 /* NSStyleKit.swift in Sources */, + 6ED248CC28D7A3BF00F78D07 /* ActivityIndicatorView.swift in Sources */, + 6ED248CA28D79DDA00F78D07 /* LockEventView.swift in Sources */, 6E4CB61028D7867700116573 /* LockRowView.swift in Sources */, 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */, 6E3276D228D70CE100AF171B /* Store.swift in Sources */, From cec8e7609251dbb60f90c2f7113543b81b445188 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 12:29:19 -0700 Subject: [PATCH 049/229] [App] Added `ActivityIndicatorView` --- .../View/UIKit/ActivityIndicatorView.swift | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 Xcode/LockKit/View/UIKit/ActivityIndicatorView.swift diff --git a/Xcode/LockKit/View/UIKit/ActivityIndicatorView.swift b/Xcode/LockKit/View/UIKit/ActivityIndicatorView.swift new file mode 100644 index 00000000..5971d8c9 --- /dev/null +++ b/Xcode/LockKit/View/UIKit/ActivityIndicatorView.swift @@ -0,0 +1,39 @@ +// +// ActivityIndicatorView.swift +// LockKit +// +// Created by Alsey Coleman Miller on 9/18/22. +// + +#if canImport(UIKit) +import SwiftUI +import UIKit + +struct ActivityIndicatorView: View, UIViewRepresentable { + + let style: UIActivityIndicatorView.Style + + public func makeUIView(context: Context) -> UIActivityIndicatorView { + let view = UIActivityIndicatorView(style: style) + view.startAnimating() + return view + } + + public func updateUIView(_ view: UIActivityIndicatorView, context: Context) { + view.style = style + if view.isAnimating == false { + view.startAnimating() + } + } +} + +struct ActivityIndicatorView_Previews: PreviewProvider { + static var previews: some View { + Group { + ActivityIndicatorView(style: .large) + ActivityIndicatorView(style: .medium) + } + } +} + +#endif From d82f8ba0b1e593bf310ad9e4e7778b4b43df4b70 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 12:43:39 -0700 Subject: [PATCH 050/229] [App] Updated `LockRowView` --- Xcode/LockKit/View/LockRowView.swift | 47 ++++++++++++++++++----- Xcode/SmartLock.xcodeproj/project.pbxproj | 4 -- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/Xcode/LockKit/View/LockRowView.swift b/Xcode/LockKit/View/LockRowView.swift index fffd8e2f..d5c9ee31 100644 --- a/Xcode/LockKit/View/LockRowView.swift +++ b/Xcode/LockKit/View/LockRowView.swift @@ -16,6 +16,8 @@ public struct LockRowView: View { public let subtitle: String? + public let trailing: (String, String)? + public var body: some View { HStack(alignment: .center, spacing: 16) { VStack { @@ -31,8 +33,31 @@ public struct LockRowView: View { .foregroundColor(.gray) } } + if let trailing = self.trailing { + Spacer(minLength: 1) + VStack(alignment: .trailing, spacing: 8) { + Text(verbatim: trailing.0) + .font(.system(size: 14)) + .foregroundColor(.gray) + Text(verbatim: trailing.1) + .font(.system(size: 14)) + .foregroundColor(.gray) + } + } } } + + public init( + image: Image, + title: String, + subtitle: String? = nil, + trailing: (String, String)? = nil + ) { + self.image = image + self.title = title + self.subtitle = subtitle + self.trailing = trailing + } } public extension LockRowView { @@ -103,18 +128,17 @@ struct LockRowView_Previews: PreviewProvider { List { LockRowView( image: .loading, - title: "Loading...", - subtitle: nil + title: "Loading..." ) LockRowView( image: .permission(.admin), - title: "Lock Name", - subtitle: "Anytime" + title: "Setup", + subtitle: "D39FE551-523F-4F64-96FC-4B828A1F8561" ) LockRowView( image: .permission(.admin), - title: "Office door", - subtitle: "Admin" + title: "Lock Name", + subtitle: "Anytime" ) LockRowView( image: .permission(.owner), @@ -126,15 +150,20 @@ struct LockRowView_Previews: PreviewProvider { title: "Home", subtitle: "Anytime" ) + LockRowView( + image: .permission(.scheduled), + title: "Office", + subtitle: "Scheduled" + ) LockRowView( image: .emoji("🔓"), title: "Unlock", - subtitle: "By Alsey Coleman Miller" + subtitle: "By Alsey Coleman Miller", + trailing: ("Today", "9:00AM") ) LockRowView( image: .symbol("bonjour"), - title: "Bonjour", - subtitle: nil + title: "Bonjour" ) .symbolRenderingMode(.multicolor) } diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index b9855f73..4bfb9bce 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -28,7 +28,6 @@ 6EA776A028D707FE00018FA3 /* LockKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 6EA7769F28D707FE00018FA3 /* LockKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6EA776A328D707FE00018FA3 /* LockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; }; 6EA776A428D707FE00018FA3 /* LockKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA7769D28D707FE00018FA3 /* LockKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 6ED248CA28D79DDA00F78D07 /* LockEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ED248C928D79DDA00F78D07 /* LockEventView.swift */; }; 6ED248CC28D7A3BF00F78D07 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ED248CB28D7A3BF00F78D07 /* ActivityIndicatorView.swift */; }; /* End PBXBuildFile section */ @@ -79,7 +78,6 @@ 6EA7769128D7061600018FA3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 6EA7769D28D707FE00018FA3 /* LockKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LockKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6EA7769F28D707FE00018FA3 /* LockKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LockKit.h; sourceTree = ""; }; - 6ED248C928D79DDA00F78D07 /* LockEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockEventView.swift; sourceTree = ""; }; 6ED248CB28D7A3BF00F78D07 /* ActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -161,7 +159,6 @@ 6E4CB61D28D7901D00116573 /* AppKit */, 6E4CB61C28D788B900116573 /* UIKit */, 6E3276E528D782B900AF171B /* LockRowView.swift */, - 6ED248C928D79DDA00F78D07 /* LockEventView.swift */, 6E4CB61928D788AA00116573 /* PermissionIconView.swift */, ); path = View; @@ -416,7 +413,6 @@ files = ( 6E4CB61F28D7902600116573 /* NSStyleKit.swift in Sources */, 6ED248CC28D7A3BF00F78D07 /* ActivityIndicatorView.swift in Sources */, - 6ED248CA28D79DDA00F78D07 /* LockEventView.swift in Sources */, 6E4CB61028D7867700116573 /* LockRowView.swift in Sources */, 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */, 6E3276D228D70CE100AF171B /* Store.swift in Sources */, From 5011a5812c46140b9cad47ba3b4f7fe02d0ee5bf Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 18 Sep 2022 13:36:39 -0700 Subject: [PATCH 051/229] [App] Added icons --- Xcode/SmartLock.xcodeproj/project.pbxproj | 12 +++++++++ .../AccentColor.colorset/Contents.json | 9 +++++++ .../AppIcon.appiconset/Contents.json | 17 +++++++++++++ .../AppIcon.appiconset/icon_128x128.png | Bin 0 -> 8222 bytes .../AppIcon.appiconset/icon_128x128@2x.png | Bin 0 -> 17277 bytes .../AppIcon.appiconset/icon_16x16.png | Bin 0 -> 785 bytes .../AppIcon.appiconset/icon_16x16@2x.png | Bin 0 -> 1855 bytes .../AppIcon.appiconset/icon_256x256.png | Bin 0 -> 17277 bytes .../AppIcon.appiconset/icon_256x256@2x.png | Bin 0 -> 39384 bytes .../AppIcon.appiconset/icon_32x32.png | Bin 0 -> 1855 bytes .../AppIcon.appiconset/icon_32x32@2x.png | Bin 0 -> 4003 bytes .../AppIcon.appiconset/icon_512x512.png | Bin 0 -> 39384 bytes .../AppIcon.appiconset/icon_512x512@2x 1.png | Bin 0 -> 72501 bytes .../AppIcon.appiconset/icon_512x512@2x.png | Bin 0 -> 72501 bytes .../AppIcon.appiconset/ios-marketing.png | Bin 0 -> 61791 bytes .../Tab Bar Icons/Contents.json | 6 +++++ .../LockTabBarIcon.imageset/Contents.json | 12 +++++++++ .../lockTabBarIcon.pdf | Bin 0 -> 2697 bytes .../Contents.json | 12 +++++++++ .../lockTabBarIconSelected.pdf | Bin 0 -> 2646 bytes .../NearTabBarIcon.imageset/Contents.json | 23 ++++++++++++++++++ .../NearTabBarIcon.imageset/Near.png | Bin 0 -> 593 bytes .../NearTabBarIcon.imageset/Near@2x.png | Bin 0 -> 1367 bytes .../NearTabBarIcon.imageset/Near@3x.png | Bin 0 -> 2332 bytes .../Contents.json | 23 ++++++++++++++++++ .../NearSelected.png | Bin 0 -> 534 bytes .../NearSelected@2x.png | Bin 0 -> 1094 bytes .../NearSelected@3x.png | Bin 0 -> 1715 bytes .../SettingsTabBarIcon.imageset/Contents.json | 12 +++++++++ .../SettingsTabBarIcon.imageset/Settings.pdf | Bin 0 -> 5388 bytes .../Contents.json | 12 +++++++++ .../SettingsTabBarIconSelected.pdf | Bin 0 -> 4412 bytes .../WirelessBlueColor.colorset/Contents.json | 20 +++++++++++++++ 33 files changed, 158 insertions(+) create mode 100644 Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_128x128.png create mode 100644 Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png create mode 100644 Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_16x16.png create mode 100644 Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png create mode 100644 Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_256x256.png create mode 100644 Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png create mode 100644 Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_32x32.png create mode 100644 Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png create mode 100644 Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_512x512.png create mode 100644 Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x 1.png create mode 100644 Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png create mode 100644 Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/ios-marketing.png create mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/Contents.json create mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIcon.imageset/Contents.json create mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIcon.imageset/lockTabBarIcon.pdf create mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIconSelected.imageset/Contents.json create mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/LockTabBarIconSelected.imageset/lockTabBarIconSelected.pdf create mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Contents.json create mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Near.png create mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Near@2x.png create mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIcon.imageset/Near@3x.png create mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/Contents.json create mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/NearSelected.png create mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/NearSelected@2x.png create mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/NearTabBarIconSelected.imageset/NearSelected@3x.png create mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/SettingsTabBarIcon.imageset/Contents.json create mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/SettingsTabBarIcon.imageset/Settings.pdf create mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/SettingsTabBarIconSelected.imageset/Contents.json create mode 100644 Xcode/SmartLock/Assets.xcassets/Tab Bar Icons/SettingsTabBarIconSelected.imageset/SettingsTabBarIconSelected.pdf create mode 100644 Xcode/SmartLock/Assets.xcassets/WirelessBlueColor.colorset/Contents.json diff --git a/Xcode/SmartLock.xcodeproj/project.pbxproj b/Xcode/SmartLock.xcodeproj/project.pbxproj index 4bfb9bce..afc5e238 100644 --- a/Xcode/SmartLock.xcodeproj/project.pbxproj +++ b/Xcode/SmartLock.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 6E2182EF28D7B37000A622B3 /* TabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2182EE28D7B37000A622B3 /* TabBarView.swift */; }; + 6E2182F128D7B4FA00A622B3 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2182F028D7B4FA00A622B3 /* SettingsView.swift */; }; + 6E2182FA28D7B7FE00A622B3 /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2182F928D7B7FE00A622B3 /* Appearance.swift */; }; 6E3276BC28D708A000AF171B /* CoreLock in Frameworks */ = {isa = PBXBuildFile; productRef = 6E3276BB28D708A000AF171B /* CoreLock */; }; 6E3276C328D70A3700AF171B /* HomeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E3276C228D70A3700AF171B /* HomeKit.framework */; }; 6E3276C628D70A3700AF171B /* RequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3276C528D70A3700AF171B /* RequestHandler.swift */; }; @@ -56,6 +59,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 6E2182EE28D7B37000A622B3 /* TabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarView.swift; sourceTree = ""; }; + 6E2182F028D7B4FA00A622B3 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + 6E2182F928D7B7FE00A622B3 /* Appearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Appearance.swift; path = ../../../../iOS/LockKit/View/Appearance.swift; sourceTree = ""; }; 6E3276BA28D7088D00AF171B /* SmartLock */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SmartLock; path = ..; sourceTree = ""; }; 6E3276C128D70A3700AF171B /* MatterLock.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MatterLock.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6E3276C228D70A3700AF171B /* HomeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HomeKit.framework; path = Library/Frameworks/HomeKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -132,6 +138,8 @@ isa = PBXGroup; children = ( 6E3276CF28D70C9400AF171B /* NearbyDevicesView.swift */, + 6E2182EE28D7B37000A622B3 /* TabBarView.swift */, + 6E2182F028D7B4FA00A622B3 /* SettingsView.swift */, ); path = View; sourceTree = ""; @@ -170,6 +178,7 @@ 6E4CB61828D788AA00116573 /* UIStyleKit.swift */, 6E4CB62028D7907200116573 /* PermissionIconViewUIView.swift */, 6ED248CB28D7A3BF00F78D07 /* ActivityIndicatorView.swift */, + 6E2182F928D7B7FE00A622B3 /* Appearance.swift */, ); path = UIKit; sourceTree = ""; @@ -401,8 +410,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6E2182F128D7B4FA00A622B3 /* SettingsView.swift in Sources */, 6E4CB62528D792BF00116573 /* InfoPlist.swift in Sources */, 6EA7768528D7061600018FA3 /* App.swift in Sources */, + 6E2182EF28D7B37000A622B3 /* TabBarView.swift in Sources */, 6E3276D028D70C9400AF171B /* NearbyDevicesView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -416,6 +427,7 @@ 6E4CB61028D7867700116573 /* LockRowView.swift in Sources */, 6E4CB62328D7907E00116573 /* PermissionIconViewNSView.swift in Sources */, 6E3276D228D70CE100AF171B /* Store.swift in Sources */, + 6E2182FA28D7B7FE00A622B3 /* Appearance.swift in Sources */, 6E4CB61A28D788AA00116573 /* UIStyleKit.swift in Sources */, 6E3276DC28D7195400AF171B /* Central.swift in Sources */, 6E4CB61B28D788AA00116573 /* PermissionIconView.swift in Sources */, diff --git a/Xcode/SmartLock/Assets.xcassets/AccentColor.colorset/Contents.json b/Xcode/SmartLock/Assets.xcassets/AccentColor.colorset/Contents.json index eb878970..95bfff08 100644 --- a/Xcode/SmartLock/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Xcode/SmartLock/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,15 @@ { "colors" : [ { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.976", + "green" : "0.506", + "red" : "0.278" + } + }, "idiom" : "universal" } ], diff --git a/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/Contents.json b/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/Contents.json index 532cd729..2ca1034e 100644 --- a/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,59 +1,76 @@ { "images" : [ { + "filename" : "ios-marketing.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { + "filename" : "icon_16x16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { + "filename" : "icon_16x16@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { + "filename" : "icon_32x32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { + "filename" : "icon_32x32@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { + "filename" : "icon_128x128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { + "filename" : "icon_128x128@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { + "filename" : "icon_256x256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { + "filename" : "icon_256x256@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { + "filename" : "icon_512x512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { + "filename" : "icon_512x512@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" + }, + { + "filename" : "icon_512x512@2x 1.png", + "idiom" : "universal", + "platform" : "watchos", + "size" : "1024x1024" } ], "info" : { diff --git a/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..2c950c8773998f75769f25b1434e4d90ba54b806 GIT binary patch literal 8222 zcmZ{}Ra6`d6D&Bm2Db^W!Gi`1Ft|Gj1b0ht_rZq2T|?(-6{ID!y%0_eG19eXQ#$V2b(E$j*fAqzzabM72Rp}*LkH;a_#L>N6C=j_5-rTk z++<~X3ADGkavlh*#Z;IkqnRd~EV-B5LL%8BMRL(up-?Ou{gBoAHqX5`V+{q38T~(p zUp?#R9&G*`a(D{ut+9399(9Q)ZO9CLg*o|ktaXJMHRJ1l}IrL`Qr8yJ$@s7 zE(_;{xb@zL4y;#igo(}9e}Sm?sDT^AMp_&1`W4RNB`dmPdhy8B(0jZqZm0l|B$i&@ltF?qq##e@T2%ViHd(#GpuSr)-$e7X*e*nZ>jz>Px zoXarI_H@U7o=S{A(^mIMYg@7>Ga359-~BbiQBpmJqtLrLXmPoQ`!7pFnanvLv5BC& zjs`EZn)l!&-GzpDd)0pV)41=g_){ZFn-O8-_iO~FQPsF!rzfiF&%M>ElSU5Z>s3u9j!GbEdb|y}N%{p_P}Tv&mVe>kROwz4QHdwm*6eVI z-7{IaZlvy%&9A5|U>+GsS)O4W;vw1(P4CHaL)8kn!@DUY80E_M>gA`Va-(<}N|kOK zu7Ge_f@>n|efvi5)xx+q^9Xika$R&-6GC#*f&(xuMRFEc`SabadMLZjw9g_q&A}HD zw}fZVi8pyJH|JXQgD~=m&5@*QW;y z+-{41rl8p3#eft>*qxJGs@<)wcg9XZ#XK;f2!Xt1`1w!M@wfCVzqyF%MZ?PWM{G7l zgCE@#g8pmnHBTb7VZZtS|Ed;dmhFyjzT>_9@EXmMsL#6u`OF#Y{_$d8W+3C6_EwXwsJtyslDwubeUvP-+G!%W} zE3;LD#(f#rIPJc5rg%x$SzT?ShmX*EzsJFPNth7-r9Ps0;T!+PqaOoR1z{%vXmQJx^;7(<@T$C;Scm8s`^ z))ka3eeR5w;b__Mvmv2%!&3aBv?jv-?%bo>>Y_q z&f-IsRaW>%oOhx66sq|%mRDANWqD*@!w{GZ)&r7=BaCN>UBx1PnL+`%+?>*HOf0vn zQqYelw7qNo1^e?_P@{L6a6CR~x+Dcnuh-fF0)0G$CetzkY6pU?5Ve$h8{SI)RF3iP zT0gOVzQy-{-o%NeoW7%CDeg5gnc?nNYKS0ZW!iMY>he6hDn}us`wq?tmoC!VB}@sp z%f^va*eP4N{u0+6=*T0oP-5@tCxYRXI>U(B9Q2xPQMtAjy$DZn4HF_-I;&J*+xnbu zKRNPRB{ggXptkSA4CBU z`rq-oXxFc{3U%K(5}0+Tz2iP$gDg0z*&AZO?L`V;c`32?M`m*DzSOkTUirLa2*N1? z3?cz&)Lo7459oEK_L6z#mBdkoM)rjtF+P|P%0Ao5k!_8bV{_*=HgF;nbetm`l8;N$ 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 literal 0 HcmV?d00001 diff --git a/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..069b7448f016e62cb540ec7ae172e0d808c5b2a5 GIT binary patch literal 17277 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 literal 0 HcmV?d00001 diff --git a/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4e9543d3025250f0a5d251f69d5d52a93ec5075d GIT binary patch literal 1855 zcmV-F2f+A=P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$_sYygZR9Fe^S7~fiRTMt=-kIr6 z7c8P#sVfQjEpH2GBQBO-e$Bm zJmp*yMjVimwzS%(b~VRKlD(zw9`)-$MQu-|kkR*rl#{XCI5r#7cWYvfV6TLVun>QY z)8H7Z>c{G=U;7kJG4JWX(rrd{oJiIRrPc^iN;~}q)60&G#K>sfV~kx>U!AeB*JNnW zrvotFej@Q{B%1N2V_OI(SmJQ-w&M*|Y8$`jN|L(S(59X_k+YCBPk9pU8k zh+m|NlCcx3>uNGLrMSVv7Yi73YkSJ2jO;PSL>NPtkBIZKh+dhcX#O-w*%_XAg8c&u z5#^1q-6GoA=+KTsnk?I!!wisMXc8RE^L16x22a}i?d3q(N|QC9AlbnCH$~gtp2PT% zLP1qa6^$Jx==4QHdyi;3{E?y^U zYH?`yVNEL+D4PC+q&16);#(a;eqcKg5N}198I-JKi#vR)timV5NU7KT5uP?LUjCeC z{Ihz89nh}A%&4anFasfML1v8TuPcVueQndJ3x-O^NLpMbDakScp=)WgJQ(v!;K2M3 zXej4f8R{Ga0rS~Nq_ z8;sDa&nmK59)t=we%{bQcDgW^xMHN6keGvRM!8T4H3?%--~i8zlA$7wGOUw^OKR&d z)WV5}F$fOiaup2a#Pn&zAU+M!Llz=xJk38$C69(DkT;U2vkzL zAVeJ|Jg$vhTphNZW3Y7!#=<%AKIc^ zL67kkAZdMacpj01uH7i^Jg38Lg`wuU+kJB6eL3g`uI%~O<{Nwl52=K1S}GE!DpH`;L% zZ25Kr1Sl>T9J$5Mb_gJ-&-ZD%!l&J;g^DVtN;g6@8Du}6_@YWMF32HzkNZRE81KyZ z>Nh7SUsoS1}N#K^Uq#3 z)Wt^%%5oqd%TMtUy@P3#T)|lQ zd#xSi*B_j~Jzzz-q}e4NXfWR1uXgob$(s<5%v|1$PKOLMe=QQm-^i4R!I1wLaH>21)2M@a&=3x|o$H<}29>|4Qa}Gu#7?wt9PugaZK~l&Rvx^?B|D z!=fTlvaQ4yNy=E~^6vQQ69LG~j+;nKB-bkySiL(b?U#<8mJ+;iv%Q@;*CctPA`Cxx{{NU@Cf zyp->Z1*v`&=)(0;Yx^euL;Bqa{bkIKezz+wx&ncFyxf-q;k64knBkV?Of*J}WRpdh tlO(V6S@Ngkzh--?vRcFP|G)Q#z`y2N!)2Ul4OIXD002ovPDHLkV1jcTU2gyY literal 0 HcmV?d00001 diff --git a/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..069b7448f016e62cb540ec7ae172e0d808c5b2a5 GIT binary patch literal 17277 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 GF6w`}FIz7F literal 0 HcmV?d00001 diff --git a/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..4e9543d3025250f0a5d251f69d5d52a93ec5075d GIT binary patch literal 1855 zcmV-F2f+A=P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$_sYygZR9Fe^S7~fiRTMt=-kIr6 z7c8P#sVfQjEpH2GBQBO-e$Bm zJmp*yMjVimwzS%(b~VRKlD(zw9`)-$MQu-|kkR*rl#{XCI5r#7cWYvfV6TLVun>QY z)8H7Z>c{G=U;7kJG4JWX(rrd{oJiIRrPc^iN;~}q)60&G#K>sfV~kx>U!AeB*JNnW zrvotFej@Q{B%1N2V_OI(SmJQ-w&M*|Y8$`jN|L(S(59X_k+YCBPk9pU8k zh+m|NlCcx3>uNGLrMSVv7Yi73YkSJ2jO;PSL>NPtkBIZKh+dhcX#O-w*%_XAg8c&u z5#^1q-6GoA=+KTsnk?I!!wisMXc8RE^L16x22a}i?d3q(N|QC9AlbnCH$~gtp2PT% zLP1qa6^$Jx==4QHdyi;3{E?y^U zYH?`yVNEL+D4PC+q&16);#(a;eqcKg5N}198I-JKi#vR)timV5NU7KT5uP?LUjCeC z{Ihz89nh}A%&4anFasfML1v8TuPcVueQndJ3x-O^NLpMbDakScp=)WgJQ(v!;K2M3 zXej4f8R{Ga0rS~Nq_ z8;sDa&nmK59)t=we%{bQcDgW^xMHN6keGvRM!8T4H3?%--~i8zlA$7wGOUw^OKR&d z)WV5}F$fOiaup2a#Pn&zAU+M!Llz=xJk38$C69(DkT;U2vkzL zAVeJ|Jg$vhTphNZW3Y7!#=<%AKIc^ zL67kkAZdMacpj01uH7i^Jg38Lg`wuU+kJB6eL3g`uI%~O<{Nwl52=K1S}GE!DpH`;L% zZ25Kr1Sl>T9J$5Mb_gJ-&-ZD%!l&J;g^DVtN;g6@8Du}6_@YWMF32HzkNZRE81KyZ z>Nh7SUsoS1}N#K^Uq#3 z)Wt^%%5oqd%TMtUy@P3#T)|lQ zd#xSi*B_j~Jzzz-q}e4NXfWR1uXgob$(s<5%v|1$PKOLMe=QQm-^i4R!I1wLaH>21)2M@a&=3x|o$H<}29>|4Qa}Gu#7?wt9PugaZK~l&Rvx^?B|D z!=fTlvaQ4yNy=E~^6vQQ69LG~j+;nKB-bkySiL(b?U#<8mJ+;iv%Q@;*CctPA`Cxx{{NU@Cf zyp->Z1*v`&=)(0;Yx^euL;Bqa{bkIKezz+wx&ncFyxf-q;k64knBkV?Of*J}WRpdh tlO(V6S@Ngkzh--?vRcFP|G)Q#z`y2N!)2Ul4OIXD002ovPDHLkV1jcTU2gyY literal 0 HcmV?d00001 diff --git a/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/Xcode/SmartLock/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..98b589395e61529b3dddf42de73a921a0c6dd373 GIT binary patch literal 4003 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