Added import and export options

Added the AccessoryController and the FindMyController to the SwiftUI Environment
This commit is contained in:
Alexander Heinrich
2021-03-15 10:36:28 +01:00
parent dda406b3d7
commit b56aa1faa7
5 changed files with 177 additions and 59 deletions
@@ -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 {
@@ -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,OpenHaystackMainView.AlertType>) -> 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 {
@@ -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
}
}
}
@@ -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)
}
}
+30 -14
View File
@@ -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}
}
}