As of iOS 13.3.1, apps installed with free developer accounts that contain embedded frameworks fail to launch. To work around this, we now link all dependencies via Cocoapods as static libraries.
275 lines
9.1 KiB
Swift
275 lines
9.1 KiB
Swift
//
|
|
// SyncManager.swift
|
|
// Delta
|
|
//
|
|
// Created by Riley Testut on 11/12/18.
|
|
// Copyright © 2018 Riley Testut. All rights reserved.
|
|
//
|
|
|
|
import Harmony
|
|
|
|
private extension UserDefaults
|
|
{
|
|
@NSManaged var didValidateHarmonyBetaDatabase: Bool
|
|
}
|
|
|
|
extension SyncManager
|
|
{
|
|
enum RecordType: String, Hashable
|
|
{
|
|
case game = "Game"
|
|
case gameCollection = "GameCollection"
|
|
case cheat = "Cheat"
|
|
case saveState = "SaveState"
|
|
case controllerSkin = "ControllerSkin"
|
|
case gameControllerInputMapping = "GameControllerInputMapping"
|
|
case gameSave = "GameSave"
|
|
|
|
var localizedName: String {
|
|
switch self
|
|
{
|
|
case .game: return NSLocalizedString("Game", comment: "")
|
|
case .gameCollection: return NSLocalizedString("Game Collection", comment: "")
|
|
case .cheat: return NSLocalizedString("Cheat", comment: "")
|
|
case .saveState: return NSLocalizedString("Save State", comment: "")
|
|
case .controllerSkin: return NSLocalizedString("Controller Skin", comment: "")
|
|
case .gameControllerInputMapping: return NSLocalizedString("Game Controller Input Mapping", comment: "")
|
|
case .gameSave: return NSLocalizedString("Game Save", comment: "")
|
|
}
|
|
}
|
|
}
|
|
|
|
enum Service: String, CaseIterable
|
|
{
|
|
case googleDrive = "com.rileytestut.Harmony.Drive"
|
|
case dropbox = "com.rileytestut.Harmony.Dropbox"
|
|
|
|
var localizedName: String {
|
|
switch self
|
|
{
|
|
case .googleDrive: return NSLocalizedString("Google Drive", comment: "")
|
|
case .dropbox: return NSLocalizedString("Dropbox", comment: "")
|
|
}
|
|
}
|
|
|
|
var service: Harmony.Service {
|
|
switch self
|
|
{
|
|
case .googleDrive: return DriveService.shared
|
|
case .dropbox: return DropboxService.shared
|
|
}
|
|
}
|
|
}
|
|
|
|
enum Error: LocalizedError
|
|
{
|
|
case nilService
|
|
|
|
var errorDescription: String? {
|
|
switch self
|
|
{
|
|
case .nilService: return NSLocalizedString("There is no chosen service for syncing.", comment: "")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Syncable where Self: NSManagedObject
|
|
{
|
|
var recordType: SyncManager.RecordType {
|
|
let recordType = SyncManager.RecordType(rawValue: self.syncableType)!
|
|
return recordType
|
|
}
|
|
}
|
|
|
|
final class SyncManager
|
|
{
|
|
static let shared = SyncManager()
|
|
|
|
var service: Service? {
|
|
guard let service = self.coordinator?.service else { return nil }
|
|
return Service(rawValue: service.identifier)
|
|
}
|
|
|
|
var recordController: RecordController? {
|
|
return self.coordinator?.recordController
|
|
}
|
|
|
|
private(set) var syncProgress: Progress?
|
|
|
|
private(set) var previousSyncResult: SyncResult?
|
|
|
|
private(set) var coordinator: SyncCoordinator?
|
|
|
|
private init()
|
|
{
|
|
DriveService.shared.clientID = "457607414709-7oc45nq59frd7rre6okq22fafftd55g1.apps.googleusercontent.com"
|
|
|
|
DropboxService.shared.clientID = "f5btgysf9ma9bb6"
|
|
DropboxService.shared.preferredDirectoryName = "Delta Emulator"
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(SyncManager.syncingDidFinish(_:)), name: SyncCoordinator.didFinishSyncingNotification, object: nil)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(SyncManager.didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(SyncManager.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
}
|
|
}
|
|
|
|
extension SyncManager
|
|
{
|
|
func start(service: Service?, completionHandler: @escaping (Result<Void, Swift.Error>) -> Void)
|
|
{
|
|
guard let service = service else { return completionHandler(.success) }
|
|
|
|
let coordinator = SyncCoordinator(service: service.service, persistentContainer: DatabaseManager.shared)
|
|
|
|
if !UserDefaults.standard.didValidateHarmonyBetaDatabase
|
|
{
|
|
UserDefaults.standard.didValidateHarmonyBetaDatabase = true
|
|
|
|
coordinator.deauthenticate { (result) in
|
|
do
|
|
{
|
|
try FileManager.default.removeItem(at: RecordController.defaultDirectoryURL())
|
|
}
|
|
catch CocoaError.fileNoSuchFile
|
|
{
|
|
// Ignore
|
|
}
|
|
catch
|
|
{
|
|
print("Failed to remove Harmony database.", error)
|
|
}
|
|
|
|
self.start(service: service, completionHandler: completionHandler)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
coordinator.start { (result) in
|
|
do
|
|
{
|
|
_ = try result.get()
|
|
|
|
self.coordinator = coordinator
|
|
completionHandler(.success)
|
|
}
|
|
catch let authError as AuthenticationError
|
|
{
|
|
// Authentication failed, but otherwise started successfully so still assign self.coordinator.
|
|
self.coordinator = coordinator
|
|
|
|
switch authError
|
|
{
|
|
case .other(ServiceError.connectionFailed):
|
|
// Authentication failed due to network connection, but otherwise started successfully so we ignore this error.
|
|
completionHandler(.success)
|
|
|
|
default:
|
|
// Another authentication error occured, so we'll deauthenticate ourselves.
|
|
print("SyncManager.start auth error:", authError)
|
|
|
|
self.deauthenticate() { (result) in
|
|
switch result
|
|
{
|
|
case .success:
|
|
completionHandler(.success)
|
|
|
|
case .failure:
|
|
// authError is more useful than result's error.
|
|
completionHandler(.failure(authError))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
print("SyncManager.start error:", error)
|
|
completionHandler(.failure(error))
|
|
}
|
|
}
|
|
}
|
|
|
|
func reset(for service: Service?, completionHandler: @escaping (Result<Void, Swift.Error>) -> Void)
|
|
{
|
|
if let coordinator = self.coordinator
|
|
{
|
|
coordinator.deauthenticate { (result) in
|
|
self.coordinator = nil
|
|
self.start(service: service, completionHandler: completionHandler)
|
|
}
|
|
}
|
|
else
|
|
{
|
|
self.start(service: service, completionHandler: completionHandler)
|
|
}
|
|
}
|
|
|
|
func authenticate(presentingViewController: UIViewController? = nil, completionHandler: @escaping (Result<Account, AuthenticationError>) -> Void)
|
|
{
|
|
guard let coordinator = self.coordinator else { return completionHandler(.failure(AuthenticationError(Error.nilService))) }
|
|
|
|
coordinator.authenticate(presentingViewController: presentingViewController) { (result) in
|
|
do
|
|
{
|
|
let account = try result.get()
|
|
|
|
if !coordinator.recordController.isSeeded
|
|
{
|
|
coordinator.recordController.seedFromPersistentContainer { (result) in
|
|
switch result
|
|
{
|
|
case .success: completionHandler(.success(account))
|
|
case .failure(let error): completionHandler(.failure(AuthenticationError(error)))
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
completionHandler(.success(account))
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
completionHandler(.failure(AuthenticationError(error)))
|
|
}
|
|
}
|
|
}
|
|
|
|
func deauthenticate(completionHandler: @escaping (Result<Void, DeauthenticationError>) -> Void)
|
|
{
|
|
guard let coordinator = self.coordinator else { return completionHandler(.success) }
|
|
|
|
coordinator.deauthenticate(completionHandler: completionHandler)
|
|
}
|
|
|
|
func sync()
|
|
{
|
|
let progress = self.coordinator?.sync()
|
|
self.syncProgress = progress
|
|
}
|
|
}
|
|
|
|
private extension SyncManager
|
|
{
|
|
@objc func syncingDidFinish(_ notification: Notification)
|
|
{
|
|
guard let result = notification.userInfo?[SyncCoordinator.syncResultKey] as? SyncResult else { return }
|
|
self.previousSyncResult = result
|
|
|
|
self.syncProgress = nil
|
|
|
|
print("Finished syncing!")
|
|
}
|
|
|
|
@objc func didEnterBackground(_ notification: Notification)
|
|
{
|
|
self.sync()
|
|
}
|
|
|
|
@objc func willEnterForeground(_ notification: Notification)
|
|
{
|
|
self.sync()
|
|
}
|
|
}
|