diff --git a/OpenHaystack/OpenHaystack/FindMy/FindMyController.swift b/OpenHaystack/OpenHaystack/FindMy/FindMyController.swift index 3d62932..69e7fba 100755 --- a/OpenHaystack/OpenHaystack/FindMy/FindMyController.swift +++ b/OpenHaystack/OpenHaystack/FindMy/FindMyController.swift @@ -15,11 +15,7 @@ import SwiftUI class FindMyController: ObservableObject { @Published var error: Error? @Published var devices = [FindMyDevice]() - var accessories: AccessoryController - - init(accessories: AccessoryController) { - self.accessories = accessories - } + @Environment(\.accessoryController) var accessories: AccessoryController func loadPrivateKeys(from data: Data, with searchPartyToken: Data, completion: @escaping (Error?) -> Void) { do { diff --git a/OpenHaystack/OpenHaystack/HaystackApp/AccessoryController.swift b/OpenHaystack/OpenHaystack/HaystackApp/AccessoryController.swift index c722b0e..5d9786a 100644 --- a/OpenHaystack/OpenHaystack/HaystackApp/AccessoryController.swift +++ b/OpenHaystack/OpenHaystack/HaystackApp/AccessoryController.swift @@ -10,11 +10,13 @@ import Combine import Foundation import SwiftUI +import OSLog class AccessoryController: ObservableObject { @Published var accessories: [Accessory] var selfObserver: AnyCancellable? var listElementsObserver = [AnyCancellable]() + @Environment(\.findMyController) var findMyController: FindMyController init(accessories: [Accessory]) { self.accessories = accessories @@ -88,6 +90,104 @@ class AccessoryController: ObservableObject { } return accessory } + + /// Export the accessories property list so it can be imported at another location + func export(accessories: [Accessory]) throws -> URL { + let propertyList = try PropertyListEncoder().encode(accessories) + + let savePanel = NSSavePanel() + savePanel.allowedFileTypes = ["plist"] + savePanel.canCreateDirectories = true + savePanel.directoryURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) + savePanel.message = "This export contains all private keys! Keep the file save to protect your location data" + savePanel.nameFieldLabel = "Filename" + savePanel.nameFieldStringValue = "openhaystack_accessories.plist" + savePanel.prompt = "Export" + savePanel.title = "Export accessories & keys" + + let result = savePanel.runModal() + + if result == .OK, + let url = savePanel.url { + // Store the accessory file + try propertyList.write(to: url) + + return url + } + throw ImportError.cancelled + } + + /// Let the user select a file to import the accessories exported by another OpenHaystack instance + func importAccessories() throws { + let openPanel = NSOpenPanel() + openPanel.allowedFileTypes = ["plist"] + openPanel.canCreateDirectories = true + openPanel.directoryURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) + openPanel.message = "Import an accessories file that includes the private keys" + openPanel.prompt = "Import" + openPanel.title = "Import accessories & keys" + + let result = openPanel.runModal() + if result == .OK, + let url = openPanel.url { + let propertyList = try Data(contentsOf: url) + var importedAccessories = try PropertyListDecoder().decode([Accessory].self, from: propertyList) + + var updatedAccessories = self.accessories + // Filter out accessories with the same id (no duplicates) + importedAccessories = importedAccessories.filter({acc in !self.accessories.contains(where: {acc.id == $0.id})}) + updatedAccessories.append(contentsOf: importedAccessories) + updatedAccessories.sort(by: {$0.name < $1.name}) + + + self.accessories = updatedAccessories + + //Update reports automatically. Do not report errors from here + self.downloadLocationReports { result in} + } + } + + enum ImportError: Error { + case cancelled + } + + + //MARK: Location reports + + + /// Download the location reports from + /// - Parameter completion: called when the reports have been succesfully downloaded or the request has failed + func downloadLocationReports(completion: @escaping (Result) -> Void) { + AnisetteDataManager.shared.requestAnisetteData { result in + switch result { + case .failure(_): + completion(.failure(.activatePlugin)) + case .success(let accountData): + let token = accountData.searchPartyToken + guard token.isEmpty == false else { + completion(.failure(.searchPartyToken)) + return + } + + self.findMyController.fetchReports(for: self.accessories, with: token) { result in + switch result { + case .failure(let error): + os_log(.error, "Downloading reports failed %@", error.localizedDescription) + completion(.failure(.downloadingReportsFailed)) + case .success(let devices): + let reports = devices.compactMap({ $0.reports }).flatMap({ $0 }) + if reports.isEmpty { + completion(.failure(.noReportsFound)) + }else { + completion(.success(())) + } + } + } + + } + } + } + } class AccessoryControllerPreview: AccessoryController { diff --git a/OpenHaystack/OpenHaystack/HaystackApp/Views/ManageAccessoriesView.swift b/OpenHaystack/OpenHaystack/HaystackApp/Views/ManageAccessoriesView.swift index e3f8e4f..d28070d 100644 --- a/OpenHaystack/OpenHaystack/HaystackApp/Views/ManageAccessoriesView.swift +++ b/OpenHaystack/OpenHaystack/HaystackApp/Views/ManageAccessoriesView.swift @@ -1,4 +1,4 @@ -// + // // OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network // // Copyright © 2021 Secure Mobile Networking Lab (SEEMOO) @@ -11,7 +11,7 @@ import SwiftUI struct ManageAccessoriesView: View { - @EnvironmentObject var accessoryController: AccessoryController + @Environment(\.accessoryController) var accessoryController: AccessoryController var accessories: [Accessory] { return self.accessoryController.accessories } @@ -39,6 +39,15 @@ struct ManageAccessoriesView: View { } .toolbar(content: { Spacer() + + Button(action: self.importAccessories, label: { + Label("Export accessories", systemImage: "square.and.arrow.down") + }) + + Button(action: self.exportAccessories, label: { + Label("Export accessories", systemImage: "square.and.arrow.up") + }) + Button(action: self.addAccessory) { Label("Add accessory", systemImage: "plus") } @@ -98,6 +107,22 @@ struct ManageAccessoriesView: View { self.alertType = .keyError } } + + func exportAccessories() { + do { + _ = try self.accessoryController.export(accessories: self.accessories) + }catch { + //TODO: Show alert + } + } + + func importAccessories() { + do { + try self.accessoryController.importAccessories() + }catch { + //TODO: Show alert + } + } } diff --git a/OpenHaystack/OpenHaystack/HaystackApp/Views/OpenHaystackMainView.swift b/OpenHaystack/OpenHaystack/HaystackApp/Views/OpenHaystackMainView.swift index 93180a2..3292cbf 100644 --- a/OpenHaystack/OpenHaystack/HaystackApp/Views/OpenHaystackMainView.swift +++ b/OpenHaystack/OpenHaystack/HaystackApp/Views/OpenHaystackMainView.swift @@ -14,8 +14,8 @@ import SwiftUI struct OpenHaystackMainView: View { @State var loading = false - @EnvironmentObject var accessoryController: AccessoryController - @EnvironmentObject var findMyController: FindMyController + @Environment(\.accessoryController) var accessoryController: AccessoryController + @Environment(\.findMyController) var findMyController: FindMyController var accessories: [Accessory] { return self.accessoryController.accessories } @@ -43,7 +43,7 @@ struct OpenHaystackMainView: View { accessoryToDeploy: self.$accessoryToDeploy, showESP32DeploySheet: self.$showESP32DeploySheet ) - .frame(minWidth: 200, idealWidth: 200, maxWidth: .infinity, minHeight: 300, idealHeight: 400, maxHeight: .infinity, alignment: .center) + .frame(minWidth: 240, idealWidth: 250, maxWidth: .infinity, minHeight: 300, idealHeight: 400, maxHeight: .infinity, alignment: .center) ZStack { AccessoryMapView(accessoryController: self.accessoryController, mapType: self.$mapType, focusedAccessory: self.focusedAccessory) @@ -135,42 +135,18 @@ struct OpenHaystackMainView: View { /// Download the location reports for all current accessories. Shows an error if something fails, like plug-in is missing func downloadLocationReports() { - - self.checkPluginIsRunning { (running) in - guard running else { - self.alertType = .activatePlugin - return - } - - guard !self.searchPartyToken.isEmpty, - let tokenData = self.searchPartyToken.data(using: .utf8) - else { - self.alertType = .searchPartyToken - return - } - - withAnimation { - self.isLoading = true - } - - findMyController.fetchReports(for: accessories, with: tokenData) { result in - switch result { - case .failure(let error): - os_log(.error, "Downloading reports failed %@", error.localizedDescription) - case .success(let devices): - let reports = devices.compactMap({ $0.reports }).flatMap({ $0 }) - if reports.isEmpty { - withAnimation { - self.popUpAlertType = .noReportsFound - } - } - } - withAnimation { - self.isLoading = false + self.accessoryController.downloadLocationReports { result in + switch result { + case .failure(let alert): + if alert == .noReportsFound { + self.popUpAlertType = .noReportsFound + }else { + self.alertType = alert } + case .success(_): + break } } - } func deploy(accessory: Accessory) { @@ -327,10 +303,14 @@ struct OpenHaystackMainView: View { message: Text("Please select to which device you want to deploy"), primaryButton: microbitButton, secondaryButton: esp32Button) + case .downloadingReportsFailed: + return Alert(title: Text("Downloading locations failed"), + message: Text("We could not download any locations from Apple. Please try again later"), + dismissButton: Alert.Button.okay()) } } - enum AlertType: Int, Identifiable { + enum AlertType: Int, Identifiable, Error { var id: Int { return self.rawValue } @@ -341,6 +321,7 @@ struct OpenHaystackMainView: View { case deployedSuccessfully case deletionFailed case noReportsFound + case downloadingReportsFailed case activatePlugin case pluginInstallFailed case selectDepoyTarget @@ -353,7 +334,7 @@ struct OpenHaystackMainView_Previews: PreviewProvider { static var previews: some View { OpenHaystackMainView() - .environmentObject(accessoryController) + .environment(\.accessoryController, accessoryController) } } diff --git a/OpenHaystack/OpenHaystack/OpenHaystackApp.swift b/OpenHaystack/OpenHaystack/OpenHaystackApp.swift index 21a2de9..d35273a 100644 --- a/OpenHaystack/OpenHaystack/OpenHaystackApp.swift +++ b/OpenHaystack/OpenHaystack/OpenHaystackApp.swift @@ -11,29 +11,45 @@ import SwiftUI @main struct OpenHaystackApp: App { - @StateObject var accessoryController: AccessoryController - @StateObject var findMyController: FindMyController + @Environment(\.accessoryController) var accessoryController: AccessoryController + @Environment(\.findMyController) var findMyController: FindMyController - init() { - var accessoryController: AccessoryController - if ProcessInfo().arguments.contains("-preview") { - accessoryController = AccessoryControllerPreview(accessories: PreviewData.accessories) - } else { - accessoryController = AccessoryController() - } - self._accessoryController = StateObject(wrappedValue: accessoryController) - self._findMyController = StateObject(wrappedValue: FindMyController(accessories: accessoryController)) - } + init() {} var body: some Scene { WindowGroup { OpenHaystackMainView() - .environmentObject(accessoryController) - .environmentObject(findMyController) } .commands { SidebarCommands() } + } } + +//MARK: Environment objects +private struct FindMyControllerEnvironmentKey: EnvironmentKey { + static let defaultValue: FindMyController = FindMyController() +} + +private struct AccessoryControllerEnvironmentKey: EnvironmentKey { + static let defaultValue: AccessoryController = { + if ProcessInfo().arguments.contains("-preview") { + return AccessoryControllerPreview(accessories: PreviewData.accessories) + } else { + return AccessoryController() + } + }() +} + +extension EnvironmentValues { + var findMyController: FindMyController { + get {self[FindMyControllerEnvironmentKey]} + } + + var accessoryController: AccessoryController { + get{self[AccessoryControllerEnvironmentKey]} + set{self[AccessoryControllerEnvironmentKey] = newValue} + } +}