Repo created
This commit is contained in:
parent
4af19165ec
commit
68073add76
12458 changed files with 12350765 additions and 2 deletions
362
iphone/Maps/Core/iCloud/SynchronizaionManager.swift
Normal file
362
iphone/Maps/Core/iCloud/SynchronizaionManager.swift
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
import Combine
|
||||
|
||||
enum VoidResult {
|
||||
case success
|
||||
case failure(Error)
|
||||
}
|
||||
|
||||
enum WritingResult {
|
||||
case success
|
||||
case reloadCategoriesAtURLs([URL])
|
||||
case deleteCategoriesAtURLs([URL])
|
||||
case failure(Error)
|
||||
}
|
||||
|
||||
typealias VoidResultCompletionHandler = (VoidResult) -> Void
|
||||
typealias WritingResultCompletionHandler = (WritingResult) -> Void
|
||||
|
||||
private let kBookmarksDirectoryName = "bookmarks"
|
||||
private let kICloudSynchronizationDidChangeEnabledStateNotificationName = "iCloudSynchronizationDidChangeEnabledStateNotification"
|
||||
private let kUDDidFinishInitialCloudSynchronization = "kUDDidFinishInitialCloudSynchronization"
|
||||
|
||||
final class SynchronizationManagerState: NSObject {
|
||||
let isAvailable: Bool
|
||||
let isOn: Bool
|
||||
let error: NSError?
|
||||
|
||||
init(isAvailable: Bool, isOn: Bool, error: NSError?) {
|
||||
self.isAvailable = isAvailable
|
||||
self.isOn = isOn
|
||||
self.error = error
|
||||
}
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
final class iCloudSynchronizaionManager: NSObject {
|
||||
|
||||
fileprivate struct Observation {
|
||||
weak var observer: AnyObject?
|
||||
var onSynchronizationStateDidChangeHandler: ((SynchronizationManagerState) -> Void)?
|
||||
}
|
||||
|
||||
var statePublisher: AnyPublisher<SynchronizationManagerState, Never> {
|
||||
stateSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private let stateSubject = PassthroughSubject<SynchronizationManagerState, Never>()
|
||||
|
||||
let fileManager: FileManager
|
||||
private let localDirectoryMonitor: LocalDirectoryMonitor
|
||||
private let cloudDirectoryMonitor: CloudDirectoryMonitor
|
||||
private let settings: SettingsBridge.Type
|
||||
private let bookmarksManager: BookmarksManager
|
||||
private var synchronizationStateManager: SynchronizationStateResolver
|
||||
private var fileWriter: SynchronizationFileWriter?
|
||||
private var observers = [ObjectIdentifier: iCloudSynchronizaionManager.Observation]()
|
||||
private var synchronizationError: Error? {
|
||||
didSet { notifyObserversOnSynchronizationError(synchronizationError) }
|
||||
}
|
||||
|
||||
static private var isInitialSynchronization: Bool {
|
||||
get {
|
||||
!UserDefaults.standard.bool(forKey: kUDDidFinishInitialCloudSynchronization)
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(!newValue, forKey: kUDDidFinishInitialCloudSynchronization)
|
||||
}
|
||||
}
|
||||
|
||||
static let shared: iCloudSynchronizaionManager = {
|
||||
let fileManager = FileManager.default
|
||||
let fileType = FileType.kml
|
||||
let cloudDirectoryMonitor = iCloudDocumentsMonitor(fileManager: fileManager, fileType: fileType)
|
||||
let synchronizationStateManager = iCloudSynchronizationStateResolver(isInitialSynchronization: isInitialSynchronization)
|
||||
do {
|
||||
let localDirectoryMonitor = try FileSystemDispatchSourceMonitor(fileManager: fileManager, directory: fileManager.bookmarksDirectoryUrl, fileType: fileType)
|
||||
let clodStorageManager = iCloudSynchronizaionManager(fileManager: fileManager,
|
||||
settings: SettingsBridge.self,
|
||||
bookmarksManager: BookmarksManager.shared(),
|
||||
cloudDirectoryMonitor: cloudDirectoryMonitor,
|
||||
localDirectoryMonitor: localDirectoryMonitor,
|
||||
synchronizationStateManager: synchronizationStateManager)
|
||||
return clodStorageManager
|
||||
} catch {
|
||||
fatalError("Failed to create shared iCloud storage manager with error: \(error)")
|
||||
}
|
||||
}()
|
||||
|
||||
// MARK: - Initialization
|
||||
init(fileManager: FileManager,
|
||||
settings: SettingsBridge.Type,
|
||||
bookmarksManager: BookmarksManager,
|
||||
cloudDirectoryMonitor: CloudDirectoryMonitor,
|
||||
localDirectoryMonitor: LocalDirectoryMonitor,
|
||||
synchronizationStateManager: SynchronizationStateResolver) {
|
||||
self.fileManager = fileManager
|
||||
self.settings = settings
|
||||
self.bookmarksManager = bookmarksManager
|
||||
self.cloudDirectoryMonitor = cloudDirectoryMonitor
|
||||
self.localDirectoryMonitor = localDirectoryMonitor
|
||||
self.synchronizationStateManager = synchronizationStateManager
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@objc func start() {
|
||||
subscribeToSettingsNotifications()
|
||||
subscribeToApplicationLifecycleNotifications()
|
||||
cloudDirectoryMonitor.delegate = self
|
||||
localDirectoryMonitor.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
private extension iCloudSynchronizaionManager {
|
||||
// MARK: - Synchronization Lifecycle
|
||||
func startSynchronization() {
|
||||
switch cloudDirectoryMonitor.state {
|
||||
case .started:
|
||||
LOG(.debug, "Synchronization is already started")
|
||||
return
|
||||
case .paused:
|
||||
resumeSynchronization()
|
||||
case .stopped:
|
||||
cloudDirectoryMonitor.start { [weak self] result in
|
||||
guard let self else { return }
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
self.processError(error)
|
||||
case .success(let cloudDirectoryUrl):
|
||||
self.localDirectoryMonitor.start { result in
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
self.processError(error)
|
||||
case .success(let localDirectoryUrl):
|
||||
LOG(.info, "Start synchronization")
|
||||
self.fileWriter = SynchronizationFileWriter(fileManager: self.fileManager,
|
||||
localDirectoryUrl: localDirectoryUrl,
|
||||
cloudDirectoryUrl: cloudDirectoryUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopSynchronization(withError error: Error? = nil) {
|
||||
LOG(.info, "Stop synchronization")
|
||||
localDirectoryMonitor.stop()
|
||||
cloudDirectoryMonitor.stop()
|
||||
fileWriter = nil
|
||||
synchronizationStateManager.resetState()
|
||||
|
||||
guard let error else { return }
|
||||
settings.setICLoudSynchronizationEnabled(false)
|
||||
synchronizationError = error
|
||||
MWMAlertViewController.activeAlert().presentBugReportAlert(withTitle: L("icloud_synchronization_error_alert_title"))
|
||||
}
|
||||
|
||||
func pauseSynchronization() {
|
||||
LOG(.info, "Pause synchronization")
|
||||
localDirectoryMonitor.pause()
|
||||
cloudDirectoryMonitor.pause()
|
||||
}
|
||||
|
||||
func resumeSynchronization() {
|
||||
LOG(.info, "Resume synchronization")
|
||||
localDirectoryMonitor.resume()
|
||||
cloudDirectoryMonitor.resume()
|
||||
}
|
||||
|
||||
// MARK: - App Lifecycle
|
||||
func subscribeToApplicationLifecycleNotifications() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground), name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(appDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||
}
|
||||
|
||||
func unsubscribeFromApplicationLifecycleNotifications() {
|
||||
NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||
}
|
||||
|
||||
func subscribeToSettingsNotifications() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(didChangeEnabledState), name: NSNotification.iCloudSynchronizationDidChangeEnabledState, object: nil)
|
||||
}
|
||||
|
||||
@objc func appWillEnterForeground() {
|
||||
guard settings.iCLoudSynchronizationEnabled() else { return }
|
||||
startSynchronization()
|
||||
}
|
||||
|
||||
@objc func appDidEnterBackground() {
|
||||
guard settings.iCLoudSynchronizationEnabled() else { return }
|
||||
pauseSynchronization()
|
||||
}
|
||||
|
||||
@objc func didChangeEnabledState() {
|
||||
if settings.iCLoudSynchronizationEnabled() {
|
||||
Self.isInitialSynchronization = true
|
||||
synchronizationStateManager.setInitialSynchronization(true)
|
||||
startSynchronization()
|
||||
} else {
|
||||
stopSynchronization()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - iCloudStorageManger + LocalDirectoryMonitorDelegate
|
||||
extension iCloudSynchronizaionManager: LocalDirectoryMonitorDelegate {
|
||||
func didFinishGathering(_ contents: LocalContents) {
|
||||
let events = synchronizationStateManager.resolveEvent(.didFinishGatheringLocalContents(contents))
|
||||
processEvents(events)
|
||||
}
|
||||
|
||||
func didUpdate(_ contents: LocalContents, _ update: LocalContentsUpdate) {
|
||||
let events = synchronizationStateManager.resolveEvent(.didUpdateLocalContents(contents: contents, update: update))
|
||||
processEvents(events)
|
||||
}
|
||||
|
||||
func didReceiveLocalMonitorError(_ error: Error) {
|
||||
processError(error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - iCloudStorageManger + CloudDirectoryMonitorDelegate
|
||||
extension iCloudSynchronizaionManager: CloudDirectoryMonitorDelegate {
|
||||
func didFinishGathering(_ contents: CloudContents) {
|
||||
let events = synchronizationStateManager.resolveEvent(.didFinishGatheringCloudContents(contents))
|
||||
processEvents(events)
|
||||
}
|
||||
|
||||
func didUpdate(_ contents: CloudContents, _ update: CloudContentsUpdate) {
|
||||
let events = synchronizationStateManager.resolveEvent(.didUpdateCloudContents(contents: contents, update: update))
|
||||
processEvents(events)
|
||||
}
|
||||
|
||||
func didReceiveCloudMonitorError(_ error: Error) {
|
||||
processError(error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
private extension iCloudSynchronizaionManager {
|
||||
func processEvents(_ events: [OutgoingSynchronizationEvent]) {
|
||||
guard !events.isEmpty else {
|
||||
synchronizationError = nil
|
||||
return
|
||||
}
|
||||
events.forEach { [weak self] event in
|
||||
guard let self, let fileWriter else { return }
|
||||
fileWriter.processEvent(event, completion: writingResultHandler(for: event))
|
||||
}
|
||||
}
|
||||
|
||||
func writingResultHandler(for event: OutgoingSynchronizationEvent) -> WritingResultCompletionHandler {
|
||||
return { [weak self] result in
|
||||
guard let self else { return }
|
||||
switch result {
|
||||
case .success:
|
||||
if case .didFinishInitialSynchronization = event {
|
||||
Self.isInitialSynchronization = false
|
||||
}
|
||||
case .reloadCategoriesAtURLs(let urls):
|
||||
urls.forEach { self.bookmarksManager.reloadCategory(atFilePath: $0.path) }
|
||||
case .deleteCategoriesAtURLs(let urls):
|
||||
urls.forEach { self.bookmarksManager.deleteCategory(atFilePath: $0.path) }
|
||||
case .failure(let error):
|
||||
self.processError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Error handling
|
||||
func processError(_ error: Error) {
|
||||
switch error {
|
||||
case let syncError as SynchronizationError:
|
||||
switch syncError {
|
||||
case .fileUnavailable,
|
||||
.fileNotUploadedDueToQuota,
|
||||
.ubiquityServerNotAvailable:
|
||||
LOG(.warning, "Synchronization Warning: \(syncError.localizedDescription)")
|
||||
synchronizationError = syncError
|
||||
case .iCloudIsNotAvailable:
|
||||
LOG(.warning, "Synchronization Warning: \(error.localizedDescription)")
|
||||
stopSynchronization()
|
||||
case .failedToOpenLocalDirectoryFileDescriptor,
|
||||
.failedToRetrieveLocalDirectoryContent,
|
||||
.containerNotFound,
|
||||
.failedToCreateMetadataItem,
|
||||
.failedToRetrieveMetadataQueryContent:
|
||||
LOG(.error, "Synchronization Error: \(error.localizedDescription)")
|
||||
stopSynchronization(withError: error)
|
||||
}
|
||||
default:
|
||||
LOG(.error, "System Error: \(error.localizedDescription)")
|
||||
stopSynchronization(withError: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Observation
|
||||
protocol SynchronizationStateObservation {
|
||||
func addObserver(_ observer: AnyObject, synchronizationStateDidChangeHandler: @escaping (SynchronizationManagerState) -> Void)
|
||||
func removeObserver(_ observer: AnyObject)
|
||||
}
|
||||
|
||||
extension iCloudSynchronizaionManager {
|
||||
func addObserver(_ observer: AnyObject, synchronizationStateDidChangeHandler: @escaping (SynchronizationManagerState) -> Void) {
|
||||
let id = ObjectIdentifier(observer)
|
||||
observers[id] = Observation(observer: observer, onSynchronizationStateDidChangeHandler: synchronizationStateDidChangeHandler)
|
||||
notifyObserversOnSynchronizationError(synchronizationError)
|
||||
}
|
||||
|
||||
func removeObserver(_ observer: AnyObject) {
|
||||
let id = ObjectIdentifier(observer)
|
||||
observers.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
func notifyObservers() {
|
||||
notifyObserversOnSynchronizationError(synchronizationError)
|
||||
}
|
||||
|
||||
private func notifyObserversOnSynchronizationError(_ error: Error?) {
|
||||
let state = SynchronizationManagerState(isAvailable: cloudDirectoryMonitor.isCloudAvailable(),
|
||||
isOn: settings.iCLoudSynchronizationEnabled(),
|
||||
error: error as? NSError)
|
||||
stateSubject.send(state)
|
||||
|
||||
observers.removeUnreachable().forEach { _, observable in
|
||||
DispatchQueue.main.async {
|
||||
observable.onSynchronizationStateDidChangeHandler?(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - FileManager + Directories
|
||||
extension FileManager {
|
||||
var bookmarksDirectoryUrl: URL {
|
||||
urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(kBookmarksDirectoryName, isDirectory: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notification + iCloudSynchronizationDidChangeEnabledState
|
||||
extension Notification.Name {
|
||||
static let iCloudSynchronizationDidChangeEnabledStateNotification = Notification.Name(kICloudSynchronizationDidChangeEnabledStateNotificationName)
|
||||
}
|
||||
|
||||
@objc extension NSNotification {
|
||||
public static let iCloudSynchronizationDidChangeEnabledState = Notification.Name.iCloudSynchronizationDidChangeEnabledStateNotification
|
||||
}
|
||||
|
||||
// MARK: - Dictionary + RemoveUnreachable
|
||||
private extension Dictionary where Key == ObjectIdentifier, Value == iCloudSynchronizaionManager.Observation {
|
||||
mutating func removeUnreachable() -> Self {
|
||||
for (id, observation) in self {
|
||||
if observation.observer == nil {
|
||||
removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue