Skip to content

Commit

Permalink
Added import and export options
Browse files Browse the repository at this point in the history
Added the AccessoryController and the FindMyController to the SwiftUI Environment
  • Loading branch information
Sn0wfreezeDev committed Mar 12, 2021
1 parent dda406b commit 63300c4
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 59 deletions.
6 changes: 1 addition & 5 deletions OpenHaystack/OpenHaystack/FindMy/FindMyController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
100 changes: 100 additions & 0 deletions OpenHaystack/OpenHaystack/HaystackApp/AccessoryController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//
//
// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
//
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
Expand All @@ -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
}
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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
}
}

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
Expand All @@ -341,6 +321,7 @@ struct OpenHaystackMainView: View {
case deployedSuccessfully
case deletionFailed
case noReportsFound
case downloadingReportsFailed
case activatePlugin
case pluginInstallFailed
case selectDepoyTarget
Expand All @@ -353,7 +334,7 @@ struct OpenHaystackMainView_Previews: PreviewProvider {

static var previews: some View {
OpenHaystackMainView()
.environmentObject(accessoryController)
.environment(\.accessoryController, accessoryController)
}
}

Expand Down
44 changes: 30 additions & 14 deletions OpenHaystack/OpenHaystack/OpenHaystackApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}
}

0 comments on commit 63300c4

Please sign in to comment.