From 432ec309f6e6ed76d27859c902f79da0e1fe43f8 Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn Date: Mon, 28 Oct 2024 12:40:06 +0400 Subject: [PATCH] [ios] refactor icloud synchronization 1. remove the iCLoud's .Trash dir scanning because on ios this feature is blocked. This is why the `isRemoved` property was removed from the `MitadataItem` 2. runtime monitors sends only the added/updated/deleted items lists on didUpdate and the whole content on didFinishGathering 3. because of 1, when icloud is running - it is source of truth. During the initial phase (1st enabling of enabling after disabling) all the data from the cloud and local dir will be preserved. 4. simplified the state manager's logic 5. improved logs 6. fixed test Signed-off-by: Kiryl Kaveryn --- iphone/Maps/Classes/MapsAppDelegate.mm | 2 +- ...itor.swift => CloudDirectoryMonitor.swift} | 138 +++---- ...itor.swift => LocalDirectoryMonitor.swift} | 40 +- iphone/Maps/Core/iCloud/MetadataItem.swift | 19 +- ...ager.swift => SynchronizaionManager.swift} | 106 ++--- .../iCloud/SynchronizationFileWriter.swift | 61 +-- .../iCloud/SynchronizationStateManager.swift | 264 ------------ .../iCloud/SynchronizationStateResolver.swift | 227 ++++++++++ iphone/Maps/Maps.xcodeproj/project.pbxproj | 32 +- .../DefaultLocalDirectoryMonitorTests.swift | 4 +- .../LocalDirectoryMonitorDelegateMock.swift | 4 +- .../Core/iCloudTests/MetadataItemStubs.swift | 2 - .../SynchronizationStateManagerTests.swift | 390 ++++-------------- ...iquitousDirectoryMonitorDelegateMock.swift | 4 +- .../iCloudDirectoryMonitorTests.swift | 4 +- .../SettingsTableViewiCloudSwitchCell.swift | 2 +- .../UI/Settings/MWMSettingsViewController.mm | 2 +- 17 files changed, 501 insertions(+), 800 deletions(-) rename iphone/Maps/Core/iCloud/{iCloudDocumentsDirectoryMonitor.swift => CloudDirectoryMonitor.swift} (58%) rename iphone/Maps/Core/iCloud/{DefaultLocalDirectoryMonitor.swift => LocalDirectoryMonitor.swift} (78%) rename iphone/Maps/Core/iCloud/{CloudStorageManager.swift => SynchronizaionManager.swift} (73%) delete mode 100644 iphone/Maps/Core/iCloud/SynchronizationStateManager.swift create mode 100644 iphone/Maps/Core/iCloud/SynchronizationStateResolver.swift diff --git a/iphone/Maps/Classes/MapsAppDelegate.mm b/iphone/Maps/Classes/MapsAppDelegate.mm index 3007974074..5477689e49 100644 --- a/iphone/Maps/Classes/MapsAppDelegate.mm +++ b/iphone/Maps/Classes/MapsAppDelegate.mm @@ -121,7 +121,7 @@ using namespace osm_auth_ios; [self enableTTSForTheFirstTime]; if (![MapsAppDelegate isTestsEnvironment]) - [[CloudStorageManager shared] start]; + [[iCloudSynchronizaionManager shared] start]; [[DeepLinkHandler shared] applicationDidFinishLaunching:launchOptions]; // application:openUrl:options is called later for deep links if YES is returned. diff --git a/iphone/Maps/Core/iCloud/iCloudDocumentsDirectoryMonitor.swift b/iphone/Maps/Core/iCloud/CloudDirectoryMonitor.swift similarity index 58% rename from iphone/Maps/Core/iCloud/iCloudDocumentsDirectoryMonitor.swift rename to iphone/Maps/Core/iCloud/CloudDirectoryMonitor.swift index 825b1f1e7b..d2516fac52 100644 --- a/iphone/Maps/Core/iCloud/iCloudDocumentsDirectoryMonitor.swift +++ b/iphone/Maps/Core/iCloud/CloudDirectoryMonitor.swift @@ -1,6 +1,4 @@ protocol CloudDirectoryMonitor: DirectoryMonitor { - var fileManager: FileManager { get } - var ubiquitousDocumentsDirectory: URL? { get } var delegate: CloudDirectoryMonitorDelegate? { get set } func fetchUbiquityDirectoryUrl(completion: ((Result) -> Void)?) @@ -8,17 +6,17 @@ protocol CloudDirectoryMonitor: DirectoryMonitor { } protocol CloudDirectoryMonitorDelegate : AnyObject { - func didFinishGathering(contents: CloudContents) - func didUpdate(contents: CloudContents) + func didFinishGathering(_ contents: CloudContents) + func didUpdate(_ contents: CloudContents, _ update: CloudContentsUpdate) func didReceiveCloudMonitorError(_ error: Error) } private let kUDCloudIdentityKey = "com.apple.organicmaps.UbiquityIdentityToken" private let kDocumentsDirectoryName = "Documents" -class iCloudDocumentsDirectoryMonitor: NSObject, CloudDirectoryMonitor { +final class iCloudDocumentsMonitor: NSObject, CloudDirectoryMonitor { - static let sharedContainerIdentifier: String = { + private static let sharedContainerIdentifier: String = { var identifier = "iCloud.app.organicmaps" #if DEBUG identifier.append(".debug") @@ -27,16 +25,17 @@ class iCloudDocumentsDirectoryMonitor: NSObject, CloudDirectoryMonitor { }() let containerIdentifier: String - let fileManager: FileManager + private let fileManager: FileManager private let fileType: FileType // TODO: Should be removed when the nested directory support will be implemented - private(set) var metadataQuery: NSMetadataQuery? - private(set) var ubiquitousDocumentsDirectory: URL? + private var metadataQuery: NSMetadataQuery? + private var ubiquitousDocumentsDirectory: URL? + private var previouslyChangedContents = CloudContentsUpdate() // MARK: - Public properties private(set) var state: DirectoryMonitorState = .stopped weak var delegate: CloudDirectoryMonitorDelegate? - init(fileManager: FileManager = .default, cloudContainerIdentifier: String = iCloudDocumentsDirectoryMonitor.sharedContainerIdentifier, fileType: FileType) { + init(fileManager: FileManager = .default, cloudContainerIdentifier: String = iCloudDocumentsMonitor.sharedContainerIdentifier, fileType: FileType) { self.fileManager = fileManager self.containerIdentifier = cloudContainerIdentifier self.fileType = fileType @@ -44,7 +43,6 @@ class iCloudDocumentsDirectoryMonitor: NSObject, CloudDirectoryMonitor { fetchUbiquityDirectoryUrl() subscribeOnMetadataQueryNotifications() - subscribeOnCloudAvailabilityNotifications() } // MARK: - Public methods @@ -72,6 +70,7 @@ class iCloudDocumentsDirectoryMonitor: NSObject, CloudDirectoryMonitor { LOG(.debug, "Stop cloud monitor.") stopQuery() state = .stopped + previouslyChangedContents = CloudContentsUpdate() } func resume() { @@ -131,64 +130,10 @@ class iCloudDocumentsDirectoryMonitor: NSObject, CloudDirectoryMonitor { return false } } - - class func buildMetadataQuery(for fileType: FileType) -> NSMetadataQuery { - let metadataQuery = NSMetadataQuery() - metadataQuery.notificationBatchingInterval = 1 - metadataQuery.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] - metadataQuery.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemFSNameKey, "*.\(fileType.fileExtension)") - metadataQuery.sortDescriptors = [NSSortDescriptor(key: NSMetadataItemFSNameKey, ascending: true)] - return metadataQuery - } - - class func getContentsFromNotification(_ notification: Notification) throws -> CloudContents { - guard let metadataQuery = notification.object as? NSMetadataQuery, - let metadataItems = metadataQuery.results as? [NSMetadataItem] else { - throw SynchronizationError.failedToRetrieveMetadataQueryContent - } - return try metadataItems.map { try CloudMetadataItem(metadataItem: $0) } - } - - // There are no ways to retrieve the content of iCloud's .Trash directory on the macOS because it uses different file system and place trashed content in the /Users//.Trash which cannot be observed without access. - // When we get a new notification and retrieve the metadata from the object the actual list of items in iOS contains both current and deleted files (which is in .Trash/ directory now) but on macOS we only have absence of the file. So there are no way to get list of deleted items on macOS on didFinishGathering state. - // Due to didUpdate state we can get the list of deleted items on macOS from the userInfo property but cannot get their new url. - class func getTrashContentsFromNotification(_ notification: Notification) throws -> CloudContents { - guard let removedItems = notification.userInfo?[NSMetadataQueryUpdateRemovedItemsKey] as? [NSMetadataItem] else { - LOG(.warning, "userInfo[NSMetadataQueryUpdateRemovedItemsKey] is nil") - return [] - } - LOG(.info, "Removed from the cloud content: \n\(removedItems.shortDebugDescription)") - return try removedItems.map { try CloudMetadataItem(metadataItem: $0, isRemoved: true) } - } - - class func getTrashedContentsFromTrashDirectory(fileManager: FileManager, ubiquitousDocumentsDirectory: URL) throws -> CloudContents { - // There are no ways to retrieve the content of iCloud's .Trash directory on macOS. - if #available(iOS 14.0, *), ProcessInfo.processInfo.isiOSAppOnMac { - LOG(.warning, "Trashed content is not available on macOS.") - return [] - } - let trashDirectoryUrl = try fileManager.trashDirectoryUrl(for: ubiquitousDocumentsDirectory) - let removedItems = try fileManager.contentsOfDirectory(at: trashDirectoryUrl, - includingPropertiesForKeys: [.isDirectoryKey], - options: [.skipsPackageDescendants, .skipsSubdirectoryDescendants]) - LOG(.info, "Trashed cloud content: \n\(removedItems)") - return try removedItems.map { try CloudMetadataItem(fileUrl: $0, isRemoved: true) } - } } // MARK: - Private -private extension iCloudDocumentsDirectoryMonitor { - - func subscribeOnCloudAvailabilityNotifications() { - NotificationCenter.default.addObserver(self, selector: #selector(cloudAvailabilityChanged(_:)), name: .NSUbiquityIdentityDidChange, object: nil) - } - - // TODO: - Actually this notification was never called. If user disable the iCloud for the current app during the active state the app will be relaunched. Needs to investigate additional cases when this notification can be sent. - @objc func cloudAvailabilityChanged(_ notification: Notification) { - LOG(.debug, "iCloudMonitor: Cloud availability changed to : \(isCloudAvailable())") - isCloudAvailable() ? startQuery() : stopQuery() - } - +private extension iCloudDocumentsMonitor { // MARK: - MetadataQuery func subscribeOnMetadataQueryNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(queryDidFinishGathering(_:)), name: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: nil) @@ -209,13 +154,12 @@ private extension iCloudDocumentsDirectoryMonitor { } @objc func queryDidFinishGathering(_ notification: Notification) { - guard isCloudAvailable(), let ubiquitousDocumentsDirectory else { return } + guard isCloudAvailable() else { return } metadataQuery?.disableUpdates() LOG(.debug, "Query did finish gathering") do { - let contents = try Self.getContentsFromNotification(notification) - let trashedContents = try Self.getTrashedContentsFromTrashDirectory(fileManager: fileManager, ubiquitousDocumentsDirectory: ubiquitousDocumentsDirectory) - delegate?.didFinishGathering(contents: contents + trashedContents) + let currentContents = try Self.getCurrentContents(notification) + delegate?.didFinishGathering(currentContents) } catch { delegate?.didReceiveCloudMonitorError(error) } @@ -227,18 +171,58 @@ private extension iCloudDocumentsDirectoryMonitor { metadataQuery?.disableUpdates() LOG(.debug, "Query did update") do { - let contents = try Self.getContentsFromNotification(notification) - let trashedContents = try Self.getTrashContentsFromNotification(notification) - delegate?.didUpdate(contents: contents + trashedContents) + let changedContents = try Self.getChangedContents(notification) + /* The metadataQuery can send the same changes multiple times with only uploading/downloading process updates. + This unnecessary updated should be skipped. */ + if changedContents != previouslyChangedContents { + previouslyChangedContents = changedContents + LOG(.info, "Added to the cloud content: \n\(changedContents.added.shortDebugDescription)") + LOG(.info, "Updated in the cloud content: \n\(changedContents.updated.shortDebugDescription)") + LOG(.info, "Removed from the cloud content: \n\(changedContents.removed.shortDebugDescription)") + let currentContents = try Self.getCurrentContents(notification) + delegate?.didUpdate(currentContents, changedContents) + } } catch { delegate?.didReceiveCloudMonitorError(error) } metadataQuery?.enableUpdates() } -} -fileprivate extension Array where Element == NSMetadataItem { - var shortDebugDescription: String { - map { $0.value(forAttribute: NSMetadataItemFSNameKey) as! String }.joined(separator: "\n") + static func buildMetadataQuery(for fileType: FileType) -> NSMetadataQuery { + let metadataQuery = NSMetadataQuery() + metadataQuery.notificationBatchingInterval = 1 + metadataQuery.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] + metadataQuery.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemFSNameKey, "*.\(fileType.fileExtension)") + metadataQuery.sortDescriptors = [NSSortDescriptor(key: NSMetadataItemFSNameKey, ascending: true)] + return metadataQuery + } + + static func getCurrentContents(_ notification: Notification) throws -> [CloudMetadataItem] { + guard let metadataQuery = notification.object as? NSMetadataQuery, + let metadataItems = metadataQuery.results as? [NSMetadataItem] else { + throw SynchronizationError.failedToRetrieveMetadataQueryContent + } + return try metadataItems.map { try CloudMetadataItem(metadataItem: $0) } + } + + static func getChangedContents(_ notification: Notification) throws -> CloudContentsUpdate { + guard let userInfo = notification.userInfo else { + throw SynchronizationError.failedToRetrieveMetadataQueryContent + } + let addedMetadataItems = userInfo[NSMetadataQueryUpdateAddedItemsKey] as? [NSMetadataItem] ?? [] + let updatedMetadataItems = userInfo[NSMetadataQueryUpdateChangedItemsKey] as? [NSMetadataItem] ?? [] + let removedMetadataItems = userInfo[NSMetadataQueryUpdateRemovedItemsKey] as? [NSMetadataItem] ?? [] + let addedContents = try addedMetadataItems.map { try CloudMetadataItem(metadataItem: $0) } + let updatedContents = try updatedMetadataItems.map { try CloudMetadataItem(metadataItem: $0) } + let removedContents = try removedMetadataItems.map { try CloudMetadataItem(metadataItem: $0) } + return CloudContentsUpdate(added: addedContents, updated: updatedContents, removed: removedContents) + } +} + +private extension CloudContentsUpdate { + init() { + self.added = [] + self.updated = [] + self.removed = [] } } diff --git a/iphone/Maps/Core/iCloud/DefaultLocalDirectoryMonitor.swift b/iphone/Maps/Core/iCloud/LocalDirectoryMonitor.swift similarity index 78% rename from iphone/Maps/Core/iCloud/DefaultLocalDirectoryMonitor.swift rename to iphone/Maps/Core/iCloud/LocalDirectoryMonitor.swift index c6733e7cd5..cd2ec94f86 100644 --- a/iphone/Maps/Core/iCloud/DefaultLocalDirectoryMonitor.swift +++ b/iphone/Maps/Core/iCloud/LocalDirectoryMonitor.swift @@ -14,18 +14,17 @@ protocol DirectoryMonitor: AnyObject { } protocol LocalDirectoryMonitor: DirectoryMonitor { - var fileManager: FileManager { get } var directory: URL { get } var delegate: LocalDirectoryMonitorDelegate? { get set } } protocol LocalDirectoryMonitorDelegate : AnyObject { - func didFinishGathering(contents: LocalContents) - func didUpdate(contents: LocalContents) + func didFinishGathering(_ contents: LocalContents) + func didUpdate(_ contents: LocalContents, _ update: LocalContentsUpdate) func didReceiveLocalMonitorError(_ error: Error) } -final class DefaultLocalDirectoryMonitor: LocalDirectoryMonitor { +final class FileSystemDispatchSourceMonitor: LocalDirectoryMonitor { typealias Delegate = LocalDirectoryMonitorDelegate @@ -35,14 +34,15 @@ final class DefaultLocalDirectoryMonitor: LocalDirectoryMonitor { case debounce(source: DispatchSourceFileSystemObject, timer: Timer) } - let fileManager: FileManager - let fileType: FileType + private let fileManager: FileManager + private let fileType: FileType private let resourceKeys: [URLResourceKey] = [.nameKey] private var dispatchSource: DispatchSourceFileSystemObject? private var dispatchSourceDebounceState: DispatchSourceDebounceState = .stopped private var dispatchSourceIsSuspended = false private var dispatchSourceIsResumed = false private var didFinishGatheringIsCalled = false + private var contents: LocalContents = [] // MARK: - Public properties let directory: URL @@ -98,6 +98,7 @@ final class DefaultLocalDirectoryMonitor: LocalDirectoryMonitor { didFinishGatheringIsCalled = false dispatchSourceDebounceState = .stopped state = .stopped + contents.removeAll() } func pause() { @@ -116,7 +117,6 @@ final class DefaultLocalDirectoryMonitor: LocalDirectoryMonitor { // MARK: - Private private func queueDidFire() { - LOG(.debug, "Queue did fire.") let debounceTimeInterval = 0.5 switch dispatchSourceDebounceState { case .started(let source): @@ -149,22 +149,38 @@ final class DefaultLocalDirectoryMonitor: LocalDirectoryMonitor { let files = try fileManager .contentsOfDirectory(at: directory, includingPropertiesForKeys: [.contentModificationDateKey], options: [.skipsHiddenFiles]) .filter { $0.pathExtension == fileType.fileExtension } - LOG(.info, "Local directory content: \(files.map { $0.lastPathComponent }) ") - let contents: LocalContents = try files.map { try LocalMetadataItem(fileUrl: $0) } + let currentContents = try files.map { try LocalMetadataItem(fileUrl: $0) } if !didFinishGatheringIsCalled { - didFinishGatheringIsCalled = true LOG(.debug, "didFinishGathering will be called") - delegate?.didFinishGathering(contents: contents) + didFinishGatheringIsCalled = true + contents = currentContents + delegate?.didFinishGathering(currentContents) } else { LOG(.debug, "didUpdate will be called") - delegate?.didUpdate(contents: contents) + let changedContents = Self.getChangedContents(oldContents: contents, newContents: currentContents) + contents = currentContents + LOG(.info, "Added to the local content: \n\(changedContents.added.shortDebugDescription)") + LOG(.info, "Updated in the local content: \n\(changedContents.updated.shortDebugDescription)") + LOG(.info, "Removed from the local content: \n\(changedContents.removed.shortDebugDescription)") + delegate?.didUpdate(currentContents, changedContents) } } catch { delegate?.didReceiveLocalMonitorError(error) } } + private static func getChangedContents(oldContents: LocalContents, newContents: LocalContents) -> LocalContentsUpdate { + let added = newContents.filter { !oldContents.containsByName($0) } + let updated = newContents.reduce(into: LocalContents()) { partialResult, newItem in + if let oldItem = oldContents.firstByName(newItem), newItem.lastModificationDate > oldItem.lastModificationDate { + partialResult.append(newItem) + } + } + let removed = oldContents.filter { !newContents.containsByName($0) } + return LocalContentsUpdate(added: added, updated: updated, removed: removed) + } + private func suspendDispatchSource() { if !dispatchSourceIsSuspended { LOG(.debug, "Suspend dispatch source.") diff --git a/iphone/Maps/Core/iCloud/MetadataItem.swift b/iphone/Maps/Core/iCloud/MetadataItem.swift index 9657ba220c..7302437721 100644 --- a/iphone/Maps/Core/iCloud/MetadataItem.swift +++ b/iphone/Maps/Core/iCloud/MetadataItem.swift @@ -15,7 +15,6 @@ struct CloudMetadataItem: MetadataItem { let fileUrl: URL var isDownloaded: Bool var lastModificationDate: TimeInterval - var isRemoved: Bool let downloadingError: NSError? let uploadingError: NSError? let hasUnresolvedConflicts: Bool @@ -29,7 +28,7 @@ extension LocalMetadataItem { throw SynchronizationError.failedToCreateMetadataItem } self.fileName = fileUrl.lastPathComponent - self.fileUrl = fileUrl + self.fileUrl = fileUrl.standardizedFileURL self.lastModificationDate = lastModificationDate } @@ -39,7 +38,7 @@ extension LocalMetadataItem { } extension CloudMetadataItem { - init(metadataItem: NSMetadataItem, isRemoved: Bool = false) throws { + init(metadataItem: NSMetadataItem) throws { guard let fileName = metadataItem.value(forAttribute: NSMetadataItemFSNameKey) as? String, let fileUrl = metadataItem.value(forAttribute: NSMetadataItemURLKey) as? URL, let downloadStatus = metadataItem.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String, @@ -50,16 +49,15 @@ extension CloudMetadataItem { throw SynchronizationError.failedToCreateMetadataItem } self.fileName = fileName - self.fileUrl = fileUrl + self.fileUrl = fileUrl.standardizedFileURL self.isDownloaded = downloadStatus == NSMetadataUbiquitousItemDownloadingStatusCurrent self.lastModificationDate = lastModificationDate - self.isRemoved = isRemoved || CloudMetadataItem.isInTrash(fileUrl) self.hasUnresolvedConflicts = hasUnresolvedConflicts self.downloadingError = metadataItem.value(forAttribute: NSMetadataUbiquitousItemDownloadingErrorKey) as? NSError self.uploadingError = metadataItem.value(forAttribute: NSMetadataUbiquitousItemUploadingErrorKey) as? NSError } - init(fileUrl: URL, isRemoved: Bool = false) throws { + init(fileUrl: URL) throws { let resources = try fileUrl.resourceValues(forKeys: [.nameKey, .contentModificationDateKey, .ubiquitousItemDownloadingStatusKey, @@ -73,19 +71,14 @@ extension CloudMetadataItem { throw SynchronizationError.failedToCreateMetadataItem } self.fileName = fileUrl.lastPathComponent - self.fileUrl = fileUrl + self.fileUrl = fileUrl.standardizedFileURL self.isDownloaded = downloadStatus.rawValue == NSMetadataUbiquitousItemDownloadingStatusCurrent self.lastModificationDate = lastModificationDate - self.isRemoved = isRemoved || CloudMetadataItem.isInTrash(fileUrl) self.hasUnresolvedConflicts = hasUnresolvedConflicts self.downloadingError = resources.ubiquitousItemDownloadingError self.uploadingError = resources.ubiquitousItemUploadingError } - static func isInTrash(_ fileUrl: URL) -> Bool { - fileUrl.pathComponents.contains(kTrashDirectoryName) - } - func relatedLocalItemUrl(to localContainer: URL) -> URL { localContainer.appendingPathComponent(fileName) } @@ -93,7 +86,7 @@ extension CloudMetadataItem { extension MetadataItem { var shortDebugDescription: String { - "File path: \(fileUrl.path), lastModified: \(lastModificationDate)" + "\(fileName), lastModified: \(lastModificationDate)" } } diff --git a/iphone/Maps/Core/iCloud/CloudStorageManager.swift b/iphone/Maps/Core/iCloud/SynchronizaionManager.swift similarity index 73% rename from iphone/Maps/Core/iCloud/CloudStorageManager.swift rename to iphone/Maps/Core/iCloud/SynchronizaionManager.swift index ee88187a4e..899d301238 100644 --- a/iphone/Maps/Core/iCloud/CloudStorageManager.swift +++ b/iphone/Maps/Core/iCloud/SynchronizaionManager.swift @@ -13,12 +13,11 @@ enum WritingResult { typealias VoidResultCompletionHandler = (VoidResult) -> Void typealias WritingResultCompletionHandler = (WritingResult) -> Void -let kTrashDirectoryName = ".Trash" private let kBookmarksDirectoryName = "bookmarks" private let kICloudSynchronizationDidChangeEnabledStateNotificationName = "iCloudSynchronizationDidChangeEnabledStateNotification" private let kUDDidFinishInitialCloudSynchronization = "kUDDidFinishInitialCloudSynchronization" -final class CloudStorageSynchronizationState: NSObject { +final class SynchronizationManagerState: NSObject { let isAvailable: Bool let isOn: Bool let error: NSError? @@ -30,11 +29,12 @@ final class CloudStorageSynchronizationState: NSObject { } } -@objc @objcMembers final class CloudStorageManager: NSObject { +@objcMembers +final class iCloudSynchronizaionManager: NSObject { fileprivate struct Observation { weak var observer: AnyObject? - var onSynchronizationStateDidChangeHandler: ((CloudStorageSynchronizationState) -> Void)? + var onSynchronizationStateDidChangeHandler: ((SynchronizationManagerState) -> Void)? } let fileManager: FileManager @@ -42,30 +42,35 @@ final class CloudStorageSynchronizationState: NSObject { private let cloudDirectoryMonitor: CloudDirectoryMonitor private let settings: Settings.Type private let bookmarksManager: BookmarksManager - private let synchronizationStateManager: SynchronizationStateManager + private var synchronizationStateManager: SynchronizationStateResolver private var fileWriter: SynchronizationFileWriter? - private var observers = [ObjectIdentifier: CloudStorageManager.Observation]() + private var observers = [ObjectIdentifier: iCloudSynchronizaionManager.Observation]() private var synchronizationError: Error? { didSet { notifyObserversOnSynchronizationError(synchronizationError) } } static private var isInitialSynchronization: Bool { - return !UserDefaults.standard.bool(forKey: kUDDidFinishInitialCloudSynchronization) + get { + !UserDefaults.standard.bool(forKey: kUDDidFinishInitialCloudSynchronization) + } + set { + UserDefaults.standard.set(!newValue, forKey: kUDDidFinishInitialCloudSynchronization) + } } - static let shared: CloudStorageManager = { + static let shared: iCloudSynchronizaionManager = { let fileManager = FileManager.default let fileType = FileType.kml - let cloudDirectoryMonitor = iCloudDocumentsDirectoryMonitor(fileManager: fileManager, fileType: fileType) - let synchronizationStateManager = DefaultSynchronizationStateManager(isInitialSynchronization: CloudStorageManager.isInitialSynchronization) + let cloudDirectoryMonitor = iCloudDocumentsMonitor(fileManager: fileManager, fileType: fileType) + let synchronizationStateManager = iCloudSynchronizationStateResolver(isInitialSynchronization: isInitialSynchronization) do { - let localDirectoryMonitor = try DefaultLocalDirectoryMonitor(fileManager: fileManager, directory: fileManager.bookmarksDirectoryUrl, fileType: fileType) - let clodStorageManager = try CloudStorageManager(fileManager: fileManager, - settings: Settings.self, - bookmarksManager: BookmarksManager.shared(), - cloudDirectoryMonitor: cloudDirectoryMonitor, - localDirectoryMonitor: localDirectoryMonitor, - synchronizationStateManager: synchronizationStateManager) + let localDirectoryMonitor = try FileSystemDispatchSourceMonitor(fileManager: fileManager, directory: fileManager.bookmarksDirectoryUrl, fileType: fileType) + let clodStorageManager = iCloudSynchronizaionManager(fileManager: fileManager, + settings: Settings.self, + bookmarksManager: BookmarksManager.shared(), + cloudDirectoryMonitor: cloudDirectoryMonitor, + localDirectoryMonitor: localDirectoryMonitor, + synchronizationStateManager: synchronizationStateManager) return clodStorageManager } catch { fatalError("Failed to create shared iCloud storage manager with error: \(error)") @@ -78,10 +83,7 @@ final class CloudStorageSynchronizationState: NSObject { bookmarksManager: BookmarksManager, cloudDirectoryMonitor: CloudDirectoryMonitor, localDirectoryMonitor: LocalDirectoryMonitor, - synchronizationStateManager: SynchronizationStateManager) throws { - guard fileManager === cloudDirectoryMonitor.fileManager, fileManager === localDirectoryMonitor.fileManager else { - throw NSError(domain: "CloudStorageManger", code: 0, userInfo: [NSLocalizedDescriptionKey: "File managers should be the same."]) - } + synchronizationStateManager: SynchronizationStateResolver) { self.fileManager = fileManager self.settings = settings self.bookmarksManager = bookmarksManager @@ -101,7 +103,7 @@ final class CloudStorageSynchronizationState: NSObject { } // MARK: - Private -private extension CloudStorageManager { +private extension iCloudSynchronizaionManager { // MARK: - Synchronization Lifecycle func startSynchronization() { LOG(.info, "Start synchronization...") @@ -185,19 +187,25 @@ private extension CloudStorageManager { } @objc func didChangeEnabledState() { - settings.iCLoudSynchronizationEnabled() ? startSynchronization() : stopSynchronization() + if settings.iCLoudSynchronizationEnabled() { + Self.isInitialSynchronization = true + synchronizationStateManager.setInitialSynchronization(true) + startSynchronization() + } else { + stopSynchronization() + } } } // MARK: - iCloudStorageManger + LocalDirectoryMonitorDelegate -extension CloudStorageManager: LocalDirectoryMonitorDelegate { - func didFinishGathering(contents: LocalContents) { +extension iCloudSynchronizaionManager: LocalDirectoryMonitorDelegate { + func didFinishGathering(_ contents: LocalContents) { let events = synchronizationStateManager.resolveEvent(.didFinishGatheringLocalContents(contents)) processEvents(events) } - func didUpdate(contents: LocalContents) { - let events = synchronizationStateManager.resolveEvent(.didUpdateLocalContents(contents)) + func didUpdate(_ contents: LocalContents, _ update: LocalContentsUpdate) { + let events = synchronizationStateManager.resolveEvent(.didUpdateLocalContents(contents: contents, update: update)) processEvents(events) } @@ -207,14 +215,14 @@ extension CloudStorageManager: LocalDirectoryMonitorDelegate { } // MARK: - iCloudStorageManger + CloudDirectoryMonitorDelegate -extension CloudStorageManager: CloudDirectoryMonitorDelegate { - func didFinishGathering(contents: CloudContents) { +extension iCloudSynchronizaionManager: CloudDirectoryMonitorDelegate { + func didFinishGathering(_ contents: CloudContents) { let events = synchronizationStateManager.resolveEvent(.didFinishGatheringCloudContents(contents)) processEvents(events) } - func didUpdate(contents: CloudContents) { - let events = synchronizationStateManager.resolveEvent(.didUpdateCloudContents(contents)) + func didUpdate(_ contents: CloudContents, _ update: CloudContentsUpdate) { + let events = synchronizationStateManager.resolveEvent(.didUpdateCloudContents(contents: contents, update: update)) processEvents(events) } @@ -224,8 +232,8 @@ extension CloudStorageManager: CloudDirectoryMonitorDelegate { } // MARK: - Private methods -private extension CloudStorageManager { - func processEvents(_ events: [OutgoingEvent]) { +private extension iCloudSynchronizaionManager { + func processEvents(_ events: [OutgoingSynchronizationEvent]) { guard !events.isEmpty else { synchronizationError = nil return @@ -236,14 +244,13 @@ private extension CloudStorageManager { } } - func writingResultHandler(for event: OutgoingEvent) -> WritingResultCompletionHandler { + func writingResultHandler(for event: OutgoingSynchronizationEvent) -> WritingResultCompletionHandler { return { [weak self] result in guard let self else { return } switch result { case .success: - // Mark that initial synchronization is finished. if case .didFinishInitialSynchronization = event { - UserDefaults.standard.set(true, forKey: kUDDidFinishInitialCloudSynchronization) + Self.isInitialSynchronization = false } case .reloadCategoriesAtURLs(let urls): urls.forEach { self.bookmarksManager.reloadCategory(atFilePath: $0.path) } @@ -283,9 +290,14 @@ private extension CloudStorageManager { } } -// MARK: - CloudStorageManger Observing -extension CloudStorageManager { - func addObserver(_ observer: AnyObject, synchronizationStateDidChangeHandler: @escaping (CloudStorageSynchronizationState) -> Void) { +// 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) @@ -297,9 +309,9 @@ extension CloudStorageManager { } private func notifyObserversOnSynchronizationError(_ error: Error?) { - let state = CloudStorageSynchronizationState(isAvailable: cloudDirectoryMonitor.isCloudAvailable(), - isOn: settings.iCLoudSynchronizationEnabled(), - error: error as? NSError) + let state = SynchronizationManagerState(isAvailable: cloudDirectoryMonitor.isCloudAvailable(), + isOn: settings.iCLoudSynchronizationEnabled(), + error: error as? NSError) observers.removeUnreachable().forEach { _, observable in DispatchQueue.main.async { observable.onSynchronizationStateDidChangeHandler?(state) @@ -313,14 +325,6 @@ extension FileManager { var bookmarksDirectoryUrl: URL { urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(kBookmarksDirectoryName, isDirectory: true) } - - func trashDirectoryUrl(for baseDirectoryUrl: URL) throws -> URL { - let trashDirectory = baseDirectoryUrl.appendingPathComponent(kTrashDirectoryName, isDirectory: true) - if !fileExists(atPath: trashDirectory.path) { - try createDirectory(at: trashDirectory, withIntermediateDirectories: true) - } - return trashDirectory - } } // MARK: - Notification + iCloudSynchronizationDidChangeEnabledState @@ -333,7 +337,7 @@ extension Notification.Name { } // MARK: - Dictionary + RemoveUnreachable -private extension Dictionary where Key == ObjectIdentifier, Value == CloudStorageManager.Observation { +private extension Dictionary where Key == ObjectIdentifier, Value == iCloudSynchronizaionManager.Observation { mutating func removeUnreachable() -> Self { for (id, observation) in self { if observation.observer == nil { diff --git a/iphone/Maps/Core/iCloud/SynchronizationFileWriter.swift b/iphone/Maps/Core/iCloud/SynchronizationFileWriter.swift index 147d537469..09ebd28e81 100644 --- a/iphone/Maps/Core/iCloud/SynchronizationFileWriter.swift +++ b/iphone/Maps/Core/iCloud/SynchronizationFileWriter.swift @@ -15,7 +15,7 @@ final class SynchronizationFileWriter { self.cloudDirectoryUrl = cloudDirectoryUrl } - func processEvent(_ event: OutgoingEvent, completion: @escaping WritingResultCompletionHandler) { + func processEvent(_ event: OutgoingSynchronizationEvent, completion: @escaping WritingResultCompletionHandler) { let resultCompletion: WritingResultCompletionHandler = { result in DispatchQueue.main.sync { completion(result) } } @@ -24,11 +24,11 @@ final class SynchronizationFileWriter { switch event { case .createLocalItem(let cloudMetadataItem): self.createInLocalContainer(cloudMetadataItem, completion: resultCompletion) case .updateLocalItem(let cloudMetadataItem): self.updateInLocalContainer(cloudMetadataItem, completion: resultCompletion) - case .removeLocalItem(let cloudMetadataItem): self.removeFromLocalContainer(cloudMetadataItem, completion: resultCompletion) + case .removeLocalItem(let localMetadataItem): self.removeFromLocalContainer(localMetadataItem, completion: resultCompletion) case .startDownloading(let cloudMetadataItem): self.startDownloading(cloudMetadataItem, completion: resultCompletion) case .createCloudItem(let localMetadataItem): self.createInCloudContainer(localMetadataItem, completion: resultCompletion) case .updateCloudItem(let localMetadataItem): self.updateInCloudContainer(localMetadataItem, completion: resultCompletion) - case .removeCloudItem(let localMetadataItem): self.removeFromCloudContainer(localMetadataItem, completion: resultCompletion) + case .removeCloudItem(let cloudMetadataItem): self.removeFromCloudContainer(cloudMetadataItem, completion: resultCompletion) case .resolveVersionsConflict(let cloudMetadataItem): self.resolveVersionsConflict(cloudMetadataItem, completion: resultCompletion) case .resolveInitialSynchronizationConflict(let localMetadataItem): self.resolveInitialSynchronizationConflict(localMetadataItem, completion: resultCompletion) case .didFinishInitialSynchronization: resultCompletion(.success) @@ -63,7 +63,7 @@ final class SynchronizationFileWriter { } private func writeToLocalContainer(_ cloudMetadataItem: CloudMetadataItem, completion: @escaping WritingResultCompletionHandler) { - LOG(.info, "Start writing file \(cloudMetadataItem.fileName) to the local directory...") + LOG(.info, "Write file \(cloudMetadataItem.fileName) to the local directory") var coordinationError: NSError? let targetLocalFileUrl = cloudMetadataItem.relatedLocalItemUrl(to: localDirectoryUrl) fileCoordinator.coordinate(readingItemAt: cloudMetadataItem.fileUrl, writingItemAt: targetLocalFileUrl, error: &coordinationError) { readingUrl, writingUrl in @@ -81,22 +81,21 @@ final class SynchronizationFileWriter { } } - private func removeFromLocalContainer(_ cloudMetadataItem: CloudMetadataItem, completion: @escaping WritingResultCompletionHandler) { - LOG(.info, "Start removing file \(cloudMetadataItem.fileName) from the local directory...") - let targetLocalFileUrl = cloudMetadataItem.relatedLocalItemUrl(to: localDirectoryUrl) + private func removeFromLocalContainer(_ localMetadataItem: LocalMetadataItem, completion: @escaping WritingResultCompletionHandler) { + LOG(.info, "Remove file \(localMetadataItem.fileName) from the local directory") + let targetLocalFileUrl = localMetadataItem.fileUrl guard fileManager.fileExists(atPath: targetLocalFileUrl.path) else { - LOG(.warning, "File \(cloudMetadataItem.fileName) doesn't exist in the local directory and cannot be removed.") + LOG(.warning, "File \(localMetadataItem.fileName) doesn't exist in the local directory and cannot be removed") completion(.success) return } completion(.deleteCategoriesAtURLs([targetLocalFileUrl])) - LOG(.debug, "File \(cloudMetadataItem.fileName) is removed from the local directory successfully.") } private func createInCloudContainer(_ localMetadataItem: LocalMetadataItem, completion: @escaping WritingResultCompletionHandler) { let targetCloudFileUrl = localMetadataItem.relatedCloudItemUrl(to: cloudDirectoryUrl) guard !fileManager.fileExists(atPath: targetCloudFileUrl.path) else { - LOG(.info, "File \(localMetadataItem.fileName) already exists in the cloud directory.") + LOG(.info, "File \(localMetadataItem.fileName) already exists in the cloud directory") completion(.success) return } @@ -108,13 +107,13 @@ final class SynchronizationFileWriter { } private func writeToCloudContainer(_ localMetadataItem: LocalMetadataItem, completion: @escaping WritingResultCompletionHandler) { - LOG(.info, "Start writing file \(localMetadataItem.fileName) to the cloud directory...") + LOG(.info, "Write file \(localMetadataItem.fileName) to the cloud directory") let targetCloudFileUrl = localMetadataItem.relatedCloudItemUrl(to: cloudDirectoryUrl) var coordinationError: NSError? fileCoordinator.coordinate(readingItemAt: localMetadataItem.fileUrl, writingItemAt: targetCloudFileUrl, error: &coordinationError) { readingUrl, writingUrl in do { try fileManager.replaceFileSafe(at: writingUrl, with: readingUrl) - LOG(.debug, "File \(localMetadataItem.fileName) is copied to the cloud directory successfully.") + LOG(.debug, "File \(localMetadataItem.fileName) is copied to the cloud directory successfully") completion(.success) } catch { completion(.failure(error)) @@ -126,44 +125,22 @@ final class SynchronizationFileWriter { } } - private func removeFromCloudContainer(_ localMetadataItem: LocalMetadataItem, completion: @escaping WritingResultCompletionHandler) { - LOG(.info, "Start trashing file \(localMetadataItem.fileName)...") - let targetCloudFileUrl = localMetadataItem.relatedCloudItemUrl(to: cloudDirectoryUrl) + private func removeFromCloudContainer(_ cloudMetadataItem: CloudMetadataItem, completion: @escaping WritingResultCompletionHandler) { + LOG(.info, "Trash file \(cloudMetadataItem.fileName) to the iCloud trash") + let targetCloudFileUrl = cloudMetadataItem.fileUrl guard fileManager.fileExists(atPath: targetCloudFileUrl.path) else { - LOG(.warning, "File \(localMetadataItem.fileName) doesn't exist in the cloud directory and cannot be moved to the trash.") + LOG(.warning, "File \(cloudMetadataItem.fileName) doesn't exist in the cloud directory and cannot be moved to the trash") completion(.success) return } do { - try removeDuplicatedFileFromTrashDirectoryIfNeeded(cloudDirectoryUrl: cloudDirectoryUrl, fileName: localMetadataItem.fileName) - try self.fileManager.trashItem(at: targetCloudFileUrl, resultingItemURL: nil) - LOG(.debug, "File \(localMetadataItem.fileName) was trashed successfully.") + try fileManager.trashItem(at: targetCloudFileUrl, resultingItemURL: nil) completion(.success) } catch { completion(.failure(error)) } } - // Remove duplicated file from iCloud's .Trash directory if needed. - // It's important to avoid the duplicating of names in the trash because we can't control the name of the trashed item. - private func removeDuplicatedFileFromTrashDirectoryIfNeeded(cloudDirectoryUrl: URL, fileName: String) throws { - // There are no ways to retrieve the content of iCloud's .Trash directory on macOS. - if #available(iOS 14.0, *), ProcessInfo.processInfo.isiOSAppOnMac { - return - } - LOG(.info, "Checking if the file \(fileName) is already in the trash directory...") - let trashDirectoryUrl = try fileManager.trashDirectoryUrl(for: cloudDirectoryUrl) - let fileInTrashDirectoryUrl = trashDirectoryUrl.appendingPathComponent(fileName) - let trashDirectoryContent = try fileManager.contentsOfDirectory(at: trashDirectoryUrl, - includingPropertiesForKeys: [], - options: [.skipsPackageDescendants, .skipsSubdirectoryDescendants]) - if trashDirectoryContent.contains(fileInTrashDirectoryUrl) { - LOG(.info, "File \(fileName) is already in the trash directory. Removing it...") - try fileManager.removeItem(at: fileInTrashDirectoryUrl) - LOG(.info, "File \(fileName) was removed from the trash directory successfully.") - } - } - // MARK: - Merge conflicts resolving private func resolveVersionsConflict(_ cloudMetadataItem: CloudMetadataItem, completion: @escaping WritingResultCompletionHandler) { LOG(.info, "Start resolving version conflict for file \(cloudMetadataItem.fileName)...") @@ -235,7 +212,11 @@ final class SynchronizationFileWriter { LOG(.info, "Start resolving initial sync conflict for file \(localMetadataItem.fileName) by copying with a new name...") do { let newFileUrl = generateNewFileUrl(for: localMetadataItem.fileUrl, addDeviceName: true) - try fileManager.copyItem(at: localMetadataItem.fileUrl, to: newFileUrl) + if !fileManager.fileExists(atPath: newFileUrl.path) { + try fileManager.copyItem(at: localMetadataItem.fileUrl, to: newFileUrl) + } else { + try fileManager.replaceFileSafe(at: newFileUrl, with: localMetadataItem.fileUrl) + } LOG(.info, "File \(localMetadataItem.fileName) was successfully resolved.") completion(.reloadCategoriesAtURLs([newFileUrl])) } catch { diff --git a/iphone/Maps/Core/iCloud/SynchronizationStateManager.swift b/iphone/Maps/Core/iCloud/SynchronizationStateManager.swift deleted file mode 100644 index 23aa633b14..0000000000 --- a/iphone/Maps/Core/iCloud/SynchronizationStateManager.swift +++ /dev/null @@ -1,264 +0,0 @@ -typealias MetadataItemName = String -typealias LocalContents = [LocalMetadataItem] -typealias CloudContents = [CloudMetadataItem] - -protocol SynchronizationStateManager { - var currentLocalContents: LocalContents { get } - var currentCloudContents: CloudContents { get } - var localContentsGatheringIsFinished: Bool { get } - var cloudContentGatheringIsFinished: Bool { get } - - @discardableResult - func resolveEvent(_ event: IncomingEvent) -> [OutgoingEvent] - func resetState() -} - -enum IncomingEvent { - case didFinishGatheringLocalContents(LocalContents) - case didFinishGatheringCloudContents(CloudContents) - case didUpdateLocalContents(LocalContents) - case didUpdateCloudContents(CloudContents) -} - -enum OutgoingEvent { - case createLocalItem(CloudMetadataItem) - case updateLocalItem(CloudMetadataItem) - case removeLocalItem(CloudMetadataItem) - case startDownloading(CloudMetadataItem) - case createCloudItem(LocalMetadataItem) - case updateCloudItem(LocalMetadataItem) - case removeCloudItem(LocalMetadataItem) - case didReceiveError(SynchronizationError) - case resolveVersionsConflict(CloudMetadataItem) - case resolveInitialSynchronizationConflict(LocalMetadataItem) - case didFinishInitialSynchronization -} - -final class DefaultSynchronizationStateManager: SynchronizationStateManager { - - // MARK: - Public properties - private(set) var localContentsGatheringIsFinished = false - private(set) var cloudContentGatheringIsFinished = false - - private(set) var currentLocalContents: LocalContents = [] - private(set) var currentCloudContents: CloudContents = [] { - didSet { - updateFilteredCloudContents() - } - } - - // Cached derived arrays - private var trashedCloudContents: [CloudMetadataItem] = [] - private var notTrashedCloudContents: [CloudMetadataItem] = [] - private var downloadedCloudContents: [CloudMetadataItem] = [] - private var notDownloadedCloudContents: [CloudMetadataItem] = [] - - private var isInitialSynchronization: Bool - - init(isInitialSynchronization: Bool) { - self.isInitialSynchronization = isInitialSynchronization - } - - // MARK: - Public methods - @discardableResult - func resolveEvent(_ event: IncomingEvent) -> [OutgoingEvent] { - let outgoingEvents: [OutgoingEvent] - switch event { - case .didFinishGatheringLocalContents(let contents): - localContentsGatheringIsFinished = true - outgoingEvents = resolveDidFinishGathering(localContents: contents, cloudContents: currentCloudContents) - case .didFinishGatheringCloudContents(let contents): - cloudContentGatheringIsFinished = true - outgoingEvents = resolveDidFinishGathering(localContents: currentLocalContents, cloudContents: contents) - case .didUpdateLocalContents(let contents): - outgoingEvents = resolveDidUpdateLocalContents(contents) - case .didUpdateCloudContents(let contents): - outgoingEvents = resolveDidUpdateCloudContents(contents) - } - LOG(.info, "Cloud content: \n\(currentCloudContents.shortDebugDescription)") - LOG(.info, "Local content: \n\(currentLocalContents.shortDebugDescription)") - LOG(.info, "Events to process: \n\(outgoingEvents)") - return outgoingEvents - } - - func resetState() { - LOG(.debug, "Resetting state") - currentLocalContents.removeAll() - currentCloudContents.removeAll() - localContentsGatheringIsFinished = false - cloudContentGatheringIsFinished = false - } - - // MARK: - Private methods - private func updateFilteredCloudContents() { - trashedCloudContents = currentCloudContents.filter { $0.isRemoved } - notTrashedCloudContents = currentCloudContents.filter { !$0.isRemoved } - } - - private func resolveDidFinishGathering(localContents: LocalContents, cloudContents: CloudContents) -> [OutgoingEvent] { - currentLocalContents = localContents - currentCloudContents = cloudContents - guard localContentsGatheringIsFinished, cloudContentGatheringIsFinished else { return [] } - - var outgoingEvents: [OutgoingEvent] - switch (localContents.isEmpty, cloudContents.isEmpty) { - case (true, true): - outgoingEvents = [] - case (true, false): - outgoingEvents = notTrashedCloudContents.map { .createLocalItem($0) } - case (false, true): - outgoingEvents = localContents.map { .createCloudItem($0) } - case (false, false): - var events = [OutgoingEvent]() - if isInitialSynchronization { - events.append(contentsOf: resolveInitialSynchronizationConflicts(localContents: localContents, cloudContents: cloudContents)) - } - events.append(contentsOf: resolveDidUpdateCloudContents(cloudContents)) - events.append(contentsOf: resolveDidUpdateLocalContents(localContents)) - outgoingEvents = events - } - if isInitialSynchronization { - outgoingEvents.append(.didFinishInitialSynchronization) - isInitialSynchronization = false - } - return outgoingEvents - } - - private func resolveDidUpdateLocalContents(_ localContents: LocalContents) -> [OutgoingEvent] { - let itemsToRemoveFromCloudContainer = Self.getItemsToRemoveFromCloudContainer(currentLocalContents: currentLocalContents, - newLocalContents: localContents) - let itemsToCreateInCloudContainer = Self.getItemsToCreateInCloudContainer(notTrashedCloudContents: notTrashedCloudContents, - trashedCloudContents: trashedCloudContents, - localContents: localContents) - let itemsToUpdateInCloudContainer = Self.getItemsToUpdateInCloudContainer(notTrashedCloudContents: notTrashedCloudContents, - localContents: localContents, - isInitialSynchronization: isInitialSynchronization) - - var outgoingEvents = [OutgoingEvent]() - itemsToRemoveFromCloudContainer.forEach { outgoingEvents.append(.removeCloudItem($0)) } - itemsToCreateInCloudContainer.forEach { outgoingEvents.append(.createCloudItem($0)) } - itemsToUpdateInCloudContainer.forEach { outgoingEvents.append(.updateCloudItem($0)) } - - currentLocalContents = localContents - return outgoingEvents - } - - private func resolveDidUpdateCloudContents(_ cloudContents: CloudContents) -> [OutgoingEvent] { - var outgoingEvents = [OutgoingEvent]() - currentCloudContents = cloudContents - - // 1. Handle errors - let errors = Self.getItemsWithErrors(cloudContents) - errors.forEach { outgoingEvents.append(.didReceiveError($0)) } - - // 2. Handle merge conflicts - let itemsWithUnresolvedConflicts = Self.getItemsToResolveConflicts(notTrashedCloudContents: notTrashedCloudContents) - itemsWithUnresolvedConflicts.forEach { outgoingEvents.append(.resolveVersionsConflict($0)) } - - // Merge conflicts should be resolved at first. - guard itemsWithUnresolvedConflicts.isEmpty else { - return outgoingEvents - } - - let itemsToRemoveFromLocalContainer = Self.getItemsToRemoveFromLocalContainer(notTrashedCloudContents: notTrashedCloudContents, - trashedCloudContents: trashedCloudContents, - localContents: currentLocalContents) - let itemsToCreateInLocalContainer = Self.getItemsToCreateInLocalContainer(notTrashedCloudContents: notTrashedCloudContents, - localContents: currentLocalContents) - let itemsToUpdateInLocalContainer = Self.getItemsToUpdateInLocalContainer(notTrashedCloudContents: notTrashedCloudContents, - localContents: currentLocalContents, - isInitialSynchronization: isInitialSynchronization) - - // 3. Handle not downloaded items - itemsToCreateInLocalContainer.notDownloaded.forEach { outgoingEvents.append(.startDownloading($0)) } - itemsToUpdateInLocalContainer.notDownloaded.forEach { outgoingEvents.append(.startDownloading($0)) } - - // 4. Handle downloaded items - itemsToRemoveFromLocalContainer.forEach { outgoingEvents.append(.removeLocalItem($0)) } - itemsToCreateInLocalContainer.downloaded.forEach { outgoingEvents.append(.createLocalItem($0)) } - itemsToUpdateInLocalContainer.downloaded.forEach { outgoingEvents.append(.updateLocalItem($0)) } - - return outgoingEvents - } - - private func resolveInitialSynchronizationConflicts(localContents: LocalContents, cloudContents: CloudContents) -> [OutgoingEvent] { - let itemsInInitialConflict = localContents.filter { cloudContents.containsByName($0) } - guard !itemsInInitialConflict.isEmpty else { - return [] - } - return itemsInInitialConflict.map { .resolveInitialSynchronizationConflict($0) } - } - - private static func getItemsToRemoveFromCloudContainer(currentLocalContents: LocalContents, newLocalContents: LocalContents) -> LocalContents { - currentLocalContents.filter { !newLocalContents.containsByName($0) } - } - - private static func getItemsToCreateInCloudContainer(notTrashedCloudContents: CloudContents, trashedCloudContents: CloudContents, localContents: LocalContents) -> LocalContents { - localContents.reduce(into: LocalContents()) { result, localItem in - if !notTrashedCloudContents.containsByName(localItem) && !trashedCloudContents.containsByName(localItem) { - result.append(localItem) - } else if !notTrashedCloudContents.containsByName(localItem), - let trashedCloudItem = trashedCloudContents.firstByName(localItem), - trashedCloudItem.lastModificationDate < localItem.lastModificationDate { - // If Cloud .Trash contains item and it's last modification date is less than the local item's last modification date than file should be recreated. - result.append(localItem) - } - } - } - - private static func getItemsToUpdateInCloudContainer(notTrashedCloudContents: CloudContents, localContents: LocalContents, isInitialSynchronization: Bool) -> LocalContents { - guard !isInitialSynchronization else { return [] } - // Due to the initial sync all conflicted local items will be duplicated with different name and replaced by the cloud items to avoid a data loss. - return localContents.reduce(into: LocalContents()) { result, localItem in - if let cloudItem = notTrashedCloudContents.firstByName(localItem), - localItem.lastModificationDate > cloudItem.lastModificationDate { - result.append(localItem) - } - } - } - - private static func getItemsWithErrors(_ cloudContents: CloudContents) -> [SynchronizationError] { - cloudContents.reduce(into: [SynchronizationError](), { partialResult, cloudItem in - if let downloadingError = cloudItem.downloadingError, let synchronizationError = downloadingError.ubiquitousError { - partialResult.append(synchronizationError) - } - if let uploadingError = cloudItem.uploadingError, let synchronizationError = uploadingError.ubiquitousError { - partialResult.append(synchronizationError) - } - }) - } - - private static func getItemsToRemoveFromLocalContainer(notTrashedCloudContents: CloudContents, trashedCloudContents: CloudContents, localContents: LocalContents) -> CloudContents { - trashedCloudContents.reduce(into: CloudContents()) { result, cloudItem in - // Items shouldn't be removed if newer version of the item isn't in the trash. - if let notTrashedCloudItem = notTrashedCloudContents.firstByName(cloudItem), notTrashedCloudItem.lastModificationDate > cloudItem.lastModificationDate { - return - } - if let localItemValue = localContents.firstByName(cloudItem), - cloudItem.lastModificationDate >= localItemValue.lastModificationDate { - result.append(cloudItem) - } - } - } - - private static func getItemsToCreateInLocalContainer(notTrashedCloudContents: CloudContents, localContents: LocalContents) -> CloudContents { - notTrashedCloudContents.withUnresolvedConflicts(false).filter { !localContents.containsByName($0) } - } - - private static func getItemsToUpdateInLocalContainer(notTrashedCloudContents: CloudContents, localContents: LocalContents, isInitialSynchronization: Bool) -> CloudContents { - notTrashedCloudContents.withUnresolvedConflicts(false).reduce(into: CloudContents()) { result, cloudItem in - if let localItemValue = localContents.firstByName(cloudItem) { - // Due to the initial sync all conflicted local items will be duplicated with different name and replaced by the cloud items to avoid a data loss. - if isInitialSynchronization { - result.append(cloudItem) - } else if cloudItem.lastModificationDate > localItemValue.lastModificationDate { - result.append(cloudItem) - } - } - } - } - - private static func getItemsToResolveConflicts(notTrashedCloudContents: CloudContents) -> CloudContents { - notTrashedCloudContents.withUnresolvedConflicts(true) - } -} diff --git a/iphone/Maps/Core/iCloud/SynchronizationStateResolver.swift b/iphone/Maps/Core/iCloud/SynchronizationStateResolver.swift new file mode 100644 index 0000000000..d2ba2bc9b5 --- /dev/null +++ b/iphone/Maps/Core/iCloud/SynchronizationStateResolver.swift @@ -0,0 +1,227 @@ +typealias LocalContents = [LocalMetadataItem] +typealias CloudContents = [CloudMetadataItem] +typealias LocalContentsUpdate = ContentsUpdate +typealias CloudContentsUpdate = ContentsUpdate + +struct ContentsUpdate: Equatable { + let added: [T] + let updated: [T] + let removed: [T] +} + +protocol SynchronizationStateResolver { + func setInitialSynchronization(_ isInitialSynchronization: Bool) + func resolveEvent(_ event: IncomingSynchronizationEvent) -> [OutgoingSynchronizationEvent] + func resetState() +} + +enum IncomingSynchronizationEvent { + case didFinishGatheringLocalContents(LocalContents) + case didFinishGatheringCloudContents(CloudContents) + case didUpdateLocalContents(contents: LocalContents, update: LocalContentsUpdate) + case didUpdateCloudContents(contents: CloudContents, update: CloudContentsUpdate) +} + +enum OutgoingSynchronizationEvent { + case startDownloading(CloudMetadataItem) + + case createLocalItem(with: CloudMetadataItem) + case updateLocalItem(with: CloudMetadataItem) + case removeLocalItem(LocalMetadataItem) + + case createCloudItem(with: LocalMetadataItem) + case updateCloudItem(with: LocalMetadataItem) + case removeCloudItem(CloudMetadataItem) + + case didReceiveError(SynchronizationError) + case resolveVersionsConflict(CloudMetadataItem) + case resolveInitialSynchronizationConflict(LocalMetadataItem) + case didFinishInitialSynchronization +} + +final class iCloudSynchronizationStateResolver: SynchronizationStateResolver { + + // MARK: - Public properties + private var localContentsGatheringIsFinished = false + private var cloudContentGatheringIsFinished = false + private var currentLocalContents: LocalContents = [] + private var currentCloudContents: CloudContents = [] + private var isInitialSynchronization: Bool + + init(isInitialSynchronization: Bool) { + self.isInitialSynchronization = isInitialSynchronization + } + + // MARK: - Public methods + @discardableResult + func resolveEvent(_ event: IncomingSynchronizationEvent) -> [OutgoingSynchronizationEvent] { + let outgoingEvents: [OutgoingSynchronizationEvent] + switch event { + case .didFinishGatheringLocalContents(let contents): + localContentsGatheringIsFinished = true + outgoingEvents = resolveDidFinishGathering(localContents: contents, cloudContents: currentCloudContents) + case .didFinishGatheringCloudContents(let contents): + cloudContentGatheringIsFinished = true + outgoingEvents = resolveDidFinishGathering(localContents: currentLocalContents, cloudContents: contents) + case .didUpdateLocalContents(let contents, let update): + currentLocalContents = contents + outgoingEvents = resolveDidUpdateLocalContents(update) + case .didUpdateCloudContents(let contents, let update): + currentCloudContents = contents + outgoingEvents = resolveDidUpdateCloudContents(update) + } + + LOG(.info, "Cloud contents: \(currentCloudContents.count)") + currentCloudContents.sorted(by: { $0.fileName < $1.fileName }).forEach { LOG(.info, $0.shortDebugDescription) } + LOG(.info, "Local contents: \(currentLocalContents.count)") + currentLocalContents.sorted(by: { $0.fileName < $1.fileName }).forEach { LOG(.info, $0.shortDebugDescription) } + LOG(.info, "Events to process: \(outgoingEvents.count)") + outgoingEvents.forEach { LOG(.info, $0) } + + return outgoingEvents + } + + func resetState() { + LOG(.debug, "Resetting state") + currentLocalContents.removeAll() + currentCloudContents.removeAll() + localContentsGatheringIsFinished = false + cloudContentGatheringIsFinished = false + } + + func setInitialSynchronization(_ isInitialSynchronization: Bool) { + self.isInitialSynchronization = isInitialSynchronization + } + + private func resolveDidFinishGathering(localContents: LocalContents, cloudContents: CloudContents) -> [OutgoingSynchronizationEvent] { + currentLocalContents = localContents + currentCloudContents = cloudContents + guard localContentsGatheringIsFinished, cloudContentGatheringIsFinished else { return [] } + + switch (localContents.isEmpty, cloudContents.isEmpty) { + case (true, true): + return [] + case (true, false): + return cloudContents.map { .createLocalItem(with: $0) } + case (false, true): + return localContents.map { .createCloudItem(with: $0) } + case (false, false): + var events = [OutgoingSynchronizationEvent]() + if isInitialSynchronization { + /* During the initial synchronization: + - all conflicted local and cloud items will be saved to avoid a data loss + - all items that are in the cloud but not in the local container will be created in the local container + - all items that are in the local container but not in the cloud container will be created in the cloud container + */ + localContents.forEach { localItem in + if let cloudItem = cloudContents.firstByName(localItem), localItem.lastModificationDate != cloudItem.lastModificationDate { + events.append(.resolveInitialSynchronizationConflict(localItem)) + events.append(.updateLocalItem(with: cloudItem)) + } + } + + let itemsToCreateInCloudContainer = localContents.filter { !cloudContents.containsByName($0) } + let itemsToCreateInLocalContainer = cloudContents.filter { !localContents.containsByName($0) } + events.append(contentsOf: itemsToCreateInCloudContainer.map { .createCloudItem(with: $0) }) + events.append(contentsOf: itemsToCreateInLocalContainer.map { .createLocalItem(with: $0) }) + + events.append(.didFinishInitialSynchronization) + isInitialSynchronization = false + } else { + cloudContents.getErrors.forEach { events.append(.didReceiveError($0)) } + cloudContents.withUnresolvedConflicts.forEach { events.append(.resolveVersionsConflict($0)) } + + /* During the non-initial synchronization: + - the iCloud container is considered as the source of truth and all items that are changed + in the cloud container will be updated in the local container + - itemsToCreateInCloudContainer is not present here because the new files cannot be added locally + when the app is closed and only the cloud contents can be changed (added/removed) between app launches + */ + let itemsToRemoveFromLocalContainer = localContents.filter { !cloudContents.containsByName($0) } + let itemsToCreateInLocalContainer = cloudContents.filter { !localContents.containsByName($0) } + let itemsToUpdateInLocalContainer = cloudContents.filter { cloudItem in + guard let localItem = localContents.firstByName(cloudItem) else { return false } + return cloudItem.lastModificationDate > localItem.lastModificationDate + } + let itemsToUpdateInCloudContainer = localContents.filter { localItem in + guard let cloudItem = cloudContents.firstByName(localItem) else { return false } + return localItem.lastModificationDate > cloudItem.lastModificationDate + } + + itemsToCreateInLocalContainer.notDownloaded.forEach { events.append(.startDownloading($0)) } + itemsToUpdateInLocalContainer.notDownloaded.forEach { events.append(.startDownloading($0)) } + + itemsToRemoveFromLocalContainer.forEach { events.append(.removeLocalItem($0)) } + itemsToCreateInLocalContainer.downloaded.forEach { events.append(.createLocalItem(with: $0)) } + itemsToUpdateInLocalContainer.downloaded.forEach { events.append(.updateLocalItem(with: $0)) } + itemsToUpdateInCloudContainer.forEach { events.append(.updateCloudItem(with: $0)) } + } + return events + } + } + + private func resolveDidUpdateLocalContents(_ localContents: LocalContentsUpdate) -> [OutgoingSynchronizationEvent] { + var outgoingEvents = [OutgoingSynchronizationEvent]() + localContents.removed.forEach { localItem in + guard let cloudItem = self.currentCloudContents.firstByName(localItem) else { return } + outgoingEvents.append(.removeCloudItem(cloudItem)) + } + localContents.added.forEach { localItem in + guard !self.currentCloudContents.containsByName(localItem) else { return } + outgoingEvents.append(.createCloudItem(with: localItem)) + } + localContents.updated.forEach { localItem in + guard let cloudItem = self.currentCloudContents.firstByName(localItem) else { + outgoingEvents.append(.createCloudItem(with: localItem)) + return + } + guard localItem.lastModificationDate > cloudItem.lastModificationDate else { return } + outgoingEvents.append(.updateCloudItem(with: localItem)) + } + return outgoingEvents + } + + private func resolveDidUpdateCloudContents(_ cloudContents: CloudContentsUpdate) -> [OutgoingSynchronizationEvent] { + var outgoingEvents = [OutgoingSynchronizationEvent]() + currentCloudContents.getErrors.forEach { outgoingEvents.append(.didReceiveError($0)) } + currentCloudContents.withUnresolvedConflicts.forEach { outgoingEvents.append(.resolveVersionsConflict($0)) } + + cloudContents.added.notDownloaded.forEach { outgoingEvents.append(.startDownloading($0)) } + cloudContents.updated.notDownloaded.forEach { outgoingEvents.append(.startDownloading($0)) } + + cloudContents.removed.forEach { cloudItem in + guard let localItem = self.currentLocalContents.firstByName(cloudItem) else { return } + outgoingEvents.append(.removeLocalItem(localItem)) + } + cloudContents.added.downloaded.forEach { cloudItem in + guard !self.currentLocalContents.containsByName(cloudItem) else { return } + outgoingEvents.append(.createLocalItem(with: cloudItem)) + } + cloudContents.updated.downloaded.forEach { cloudItem in + guard let localItem = self.currentLocalContents.firstByName(cloudItem) else { + outgoingEvents.append(.createLocalItem(with: cloudItem)) + return + } + guard cloudItem.lastModificationDate > localItem.lastModificationDate else { return } + outgoingEvents.append(.updateLocalItem(with: cloudItem)) + } + return outgoingEvents + } +} + +private extension CloudContents { + var withUnresolvedConflicts: CloudContents { + filter { $0.hasUnresolvedConflicts } + } + + var getErrors: [SynchronizationError] { + reduce(into: [SynchronizationError](), { partialResult, cloudItem in + if let downloadingError = cloudItem.downloadingError, let synchronizationError = downloadingError.ubiquitousError { + partialResult.append(synchronizationError) + } + if let uploadingError = cloudItem.uploadingError, let synchronizationError = uploadingError.ubiquitousError { + partialResult.append(synchronizationError) + } + }) + } +} diff --git a/iphone/Maps/Maps.xcodeproj/project.pbxproj b/iphone/Maps/Maps.xcodeproj/project.pbxproj index e08f741dc9..5b0fe55505 100644 --- a/iphone/Maps/Maps.xcodeproj/project.pbxproj +++ b/iphone/Maps/Maps.xcodeproj/project.pbxproj @@ -481,12 +481,12 @@ ED77556E2C2C490B0051E656 /* UIAlertController+openInAppActionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED77556D2C2C490B0051E656 /* UIAlertController+openInAppActionSheet.swift */; }; ED79A5AB2BD7AA9C00952D1F /* LoadingOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5AA2BD7AA9C00952D1F /* LoadingOverlayViewController.swift */; }; ED79A5AD2BD7BA0F00952D1F /* UIApplication+LoadingOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5AC2BD7BA0F00952D1F /* UIApplication+LoadingOverlay.swift */; }; - ED79A5D32BDF8D6100952D1F /* CloudStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5CB2BDF8D6100952D1F /* CloudStorageManager.swift */; }; + ED79A5D32BDF8D6100952D1F /* SynchronizaionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5CB2BDF8D6100952D1F /* SynchronizaionManager.swift */; }; ED79A5D42BDF8D6100952D1F /* MetadataItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5CC2BDF8D6100952D1F /* MetadataItem.swift */; }; ED79A5D52BDF8D6100952D1F /* SynchronizationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5CD2BDF8D6100952D1F /* SynchronizationError.swift */; }; - ED79A5D62BDF8D6100952D1F /* iCloudDocumentsDirectoryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5CE2BDF8D6100952D1F /* iCloudDocumentsDirectoryMonitor.swift */; }; - ED79A5D72BDF8D6100952D1F /* SynchronizationStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5CF2BDF8D6100952D1F /* SynchronizationStateManager.swift */; }; - ED79A5D82BDF8D6100952D1F /* DefaultLocalDirectoryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5D02BDF8D6100952D1F /* DefaultLocalDirectoryMonitor.swift */; }; + ED79A5D62BDF8D6100952D1F /* CloudDirectoryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5CE2BDF8D6100952D1F /* CloudDirectoryMonitor.swift */; }; + ED79A5D72BDF8D6100952D1F /* SynchronizationStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5CF2BDF8D6100952D1F /* SynchronizationStateResolver.swift */; }; + ED79A5D82BDF8D6100952D1F /* LocalDirectoryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5D02BDF8D6100952D1F /* LocalDirectoryMonitor.swift */; }; ED7CCC4F2C1362E300E2A737 /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED7CCC4E2C1362E300E2A737 /* FileType.swift */; }; ED808D0F2C38407800D52585 /* CircleImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED808D0E2C38407800D52585 /* CircleImageButton.swift */; }; ED8270F02C2071A3005966DA /* SettingsTableViewDetailedSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8270EF2C2071A3005966DA /* SettingsTableViewDetailedSwitchCell.swift */; }; @@ -1409,12 +1409,12 @@ ED77556D2C2C490B0051E656 /* UIAlertController+openInAppActionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+openInAppActionSheet.swift"; sourceTree = ""; }; ED79A5AA2BD7AA9C00952D1F /* LoadingOverlayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingOverlayViewController.swift; sourceTree = ""; }; ED79A5AC2BD7BA0F00952D1F /* UIApplication+LoadingOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+LoadingOverlay.swift"; sourceTree = ""; }; - ED79A5CB2BDF8D6100952D1F /* CloudStorageManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudStorageManager.swift; sourceTree = ""; }; + ED79A5CB2BDF8D6100952D1F /* SynchronizaionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizaionManager.swift; sourceTree = ""; }; ED79A5CC2BDF8D6100952D1F /* MetadataItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetadataItem.swift; sourceTree = ""; }; ED79A5CD2BDF8D6100952D1F /* SynchronizationError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizationError.swift; sourceTree = ""; }; - ED79A5CE2BDF8D6100952D1F /* iCloudDocumentsDirectoryMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iCloudDocumentsDirectoryMonitor.swift; sourceTree = ""; }; - ED79A5CF2BDF8D6100952D1F /* SynchronizationStateManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizationStateManager.swift; sourceTree = ""; }; - ED79A5D02BDF8D6100952D1F /* DefaultLocalDirectoryMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultLocalDirectoryMonitor.swift; sourceTree = ""; }; + ED79A5CE2BDF8D6100952D1F /* CloudDirectoryMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudDirectoryMonitor.swift; sourceTree = ""; }; + ED79A5CF2BDF8D6100952D1F /* SynchronizationStateResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizationStateResolver.swift; sourceTree = ""; }; + ED79A5D02BDF8D6100952D1F /* LocalDirectoryMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalDirectoryMonitor.swift; sourceTree = ""; }; ED7CCC4E2C1362E300E2A737 /* FileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileType.swift; sourceTree = ""; }; ED808D0E2C38407800D52585 /* CircleImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleImageButton.swift; sourceTree = ""; }; ED8270EF2C2071A3005966DA /* SettingsTableViewDetailedSwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTableViewDetailedSwitchCell.swift; sourceTree = ""; }; @@ -3125,11 +3125,11 @@ ED79A5CA2BDF8D6100952D1F /* iCloud */ = { isa = PBXGroup; children = ( - ED79A5CB2BDF8D6100952D1F /* CloudStorageManager.swift */, + ED79A5CB2BDF8D6100952D1F /* SynchronizaionManager.swift */, EDF838812C00B640007E4E67 /* SynchronizationFileWriter.swift */, - ED79A5CF2BDF8D6100952D1F /* SynchronizationStateManager.swift */, - ED79A5CE2BDF8D6100952D1F /* iCloudDocumentsDirectoryMonitor.swift */, - ED79A5D02BDF8D6100952D1F /* DefaultLocalDirectoryMonitor.swift */, + ED79A5CF2BDF8D6100952D1F /* SynchronizationStateResolver.swift */, + ED79A5CE2BDF8D6100952D1F /* CloudDirectoryMonitor.swift */, + ED79A5D02BDF8D6100952D1F /* LocalDirectoryMonitor.swift */, ED79A5CC2BDF8D6100952D1F /* MetadataItem.swift */, ED79A5CD2BDF8D6100952D1F /* SynchronizationError.swift */, ); @@ -4400,7 +4400,7 @@ 3454D7D41E07F045004AF2AD /* UIImageView+Coloring.m in Sources */, 993DF11D23F6BDB100AC231A /* UIToolbarRenderer.swift in Sources */, 99A906E923F6F7030005872B /* WikiDescriptionViewController.swift in Sources */, - ED79A5D62BDF8D6100952D1F /* iCloudDocumentsDirectoryMonitor.swift in Sources */, + ED79A5D62BDF8D6100952D1F /* CloudDirectoryMonitor.swift in Sources */, EDFDFB522B726F1A0013A44C /* ButtonsStackView.swift in Sources */, 993DF11023F6BDB100AC231A /* MWMButtonRenderer.swift in Sources */, 3463BA671DE81DB90082417F /* MWMTrafficButtonViewController.mm in Sources */, @@ -4550,7 +4550,7 @@ F6E2FF661E097BA00083EBEC /* MWMTTSSettingsViewController.mm in Sources */, 3454D7C21E07F045004AF2AD /* NSString+Categories.m in Sources */, 34E7761F1F14DB48003040B3 /* PlacePageArea.swift in Sources */, - ED79A5D82BDF8D6100952D1F /* DefaultLocalDirectoryMonitor.swift in Sources */, + ED79A5D82BDF8D6100952D1F /* LocalDirectoryMonitor.swift in Sources */, EDC4E34B2C5D1BEF009286A2 /* RecentlyDeletedCategoriesViewController.swift in Sources */, ED8270F02C2071A3005966DA /* SettingsTableViewDetailedSwitchCell.swift in Sources */, 4728F69322CF89A400E00028 /* GradientView.swift in Sources */, @@ -4628,7 +4628,7 @@ 998927402449ECC200260CE2 /* BottomMenuItemCell.swift in Sources */, F6E2FEE21E097BA00083EBEC /* MWMSearchManager.mm in Sources */, F6E2FE221E097BA00083EBEC /* MWMOpeningHoursEditorViewController.mm in Sources */, - ED79A5D72BDF8D6100952D1F /* SynchronizationStateManager.swift in Sources */, + ED79A5D72BDF8D6100952D1F /* SynchronizationStateResolver.swift in Sources */, 999FC12B23ABB4B800B0E6F9 /* FontStyleSheet.swift in Sources */, 47CA68DA2500469400671019 /* BookmarksListBuilder.swift in Sources */, 34D3AFE21E376F7E004100F9 /* UITableView+Updates.swift in Sources */, @@ -4671,7 +4671,7 @@ 6741AA1C1BF340DE002C974C /* MWMRoutingDisclaimerAlert.m in Sources */, 34D3B0481E389D05004100F9 /* MWMNoteCell.m in Sources */, CD9AD967228067F500EC174A /* MapInfo.swift in Sources */, - ED79A5D32BDF8D6100952D1F /* CloudStorageManager.swift in Sources */, + ED79A5D32BDF8D6100952D1F /* SynchronizaionManager.swift in Sources */, 6741AA1D1BF340DE002C974C /* MWMDownloadTransitMapAlert.mm in Sources */, 471A7BBE2481A3D000A0D4C1 /* EditBookmarkViewController.swift in Sources */, 993DF0C923F6BD0600AC231A /* ElevationDetailsBuilder.swift in Sources */, diff --git a/iphone/Maps/Tests/Core/iCloudTests/DefaultLocalDirectoryMonitorTests/DefaultLocalDirectoryMonitorTests.swift b/iphone/Maps/Tests/Core/iCloudTests/DefaultLocalDirectoryMonitorTests/DefaultLocalDirectoryMonitorTests.swift index 6219cd088f..b53e2beb94 100644 --- a/iphone/Maps/Tests/Core/iCloudTests/DefaultLocalDirectoryMonitorTests/DefaultLocalDirectoryMonitorTests.swift +++ b/iphone/Maps/Tests/Core/iCloudTests/DefaultLocalDirectoryMonitorTests/DefaultLocalDirectoryMonitorTests.swift @@ -5,13 +5,13 @@ final class DefaultLocalDirectoryMonitorTests: XCTestCase { let fileManager = FileManager.default let tempDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - var directoryMonitor: DefaultLocalDirectoryMonitor! + var directoryMonitor: FileSystemDispatchSourceMonitor! var mockDelegate: LocalDirectoryMonitorDelegateMock! override func setUpWithError() throws { try super.setUpWithError() // Setup with a temporary directory and a mock delegate - directoryMonitor = try DefaultLocalDirectoryMonitor(fileManager: fileManager, directory: tempDirectory) + directoryMonitor = try FileSystemDispatchSourceMonitor(fileManager: fileManager, directory: tempDirectory) mockDelegate = LocalDirectoryMonitorDelegateMock() directoryMonitor.delegate = mockDelegate } diff --git a/iphone/Maps/Tests/Core/iCloudTests/DefaultLocalDirectoryMonitorTests/LocalDirectoryMonitorDelegateMock.swift b/iphone/Maps/Tests/Core/iCloudTests/DefaultLocalDirectoryMonitorTests/LocalDirectoryMonitorDelegateMock.swift index 934fd86ec5..9a8d489c80 100644 --- a/iphone/Maps/Tests/Core/iCloudTests/DefaultLocalDirectoryMonitorTests/LocalDirectoryMonitorDelegateMock.swift +++ b/iphone/Maps/Tests/Core/iCloudTests/DefaultLocalDirectoryMonitorTests/LocalDirectoryMonitorDelegateMock.swift @@ -8,12 +8,12 @@ class LocalDirectoryMonitorDelegateMock: LocalDirectoryMonitorDelegate { var didUpdateExpectation: XCTestExpectation? var didReceiveErrorExpectation: XCTestExpectation? - func didFinishGathering(contents: LocalContents) { + func didFinishGathering(_ contents: LocalContents) { self.contents = contents didFinishGatheringExpectation?.fulfill() } - func didUpdate(contents: LocalContents) { + func didUpdate(_ contents: LocalContents, _ update: LocalContentsUpdate) { self.contents = contents didUpdateExpectation?.fulfill() } diff --git a/iphone/Maps/Tests/Core/iCloudTests/MetadataItemStubs.swift b/iphone/Maps/Tests/Core/iCloudTests/MetadataItemStubs.swift index c977d05bbf..302eb72215 100644 --- a/iphone/Maps/Tests/Core/iCloudTests/MetadataItemStubs.swift +++ b/iphone/Maps/Tests/Core/iCloudTests/MetadataItemStubs.swift @@ -14,14 +14,12 @@ extension LocalMetadataItem { extension CloudMetadataItem { static func stub(fileName: String, lastModificationDate: TimeInterval, - isInTrash: Bool, isDownloaded: Bool = true, hasUnresolvedConflicts: Bool = false) -> CloudMetadataItem { let item = CloudMetadataItem(fileName: fileName, fileUrl: URL(string: "url")!, isDownloaded: isDownloaded, lastModificationDate: lastModificationDate, - isRemoved: isInTrash, downloadingError: nil, uploadingError: nil, hasUnresolvedConflicts: hasUnresolvedConflicts) diff --git a/iphone/Maps/Tests/Core/iCloudTests/SynchronizationStateManagerTests.swift b/iphone/Maps/Tests/Core/iCloudTests/SynchronizationStateManagerTests.swift index 5d91b67ba9..a507743a01 100644 --- a/iphone/Maps/Tests/Core/iCloudTests/SynchronizationStateManagerTests.swift +++ b/iphone/Maps/Tests/Core/iCloudTests/SynchronizationStateManagerTests.swift @@ -1,14 +1,14 @@ import XCTest @testable import Organic_Maps__Debug_ -final class SynchronizationStateManagerTests: XCTestCase { +final class SynchronizationtateManagerTests: XCTestCase { - var syncStateManager: SynchronizationStateManager! - var outgoingEvents: [OutgoingEvent] = [] + var syncStateManager: SynchronizationStateResolver! + var outgoingEvents: [OutgoingSynchronizationEvent] = [] override func setUp() { super.setUp() - syncStateManager = DefaultSynchronizationStateManager(isInitialSynchronization: false) + syncStateManager = iCloudSynchronizationStateResolver(isInitialSynchronization: false) } override func tearDown() { @@ -18,13 +18,13 @@ final class SynchronizationStateManagerTests: XCTestCase { } // MARK: - Test didFinishGathering without errors and on initial synchronization func testInitialSynchronization() { - syncStateManager = DefaultSynchronizationStateManager(isInitialSynchronization: true) + syncStateManager = iCloudSynchronizationStateResolver(isInitialSynchronization: true) let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1)) let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(3)) // Local only item - let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(2), isInTrash: false) // Conflicting item - let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(4), isInTrash: false) // Cloud only item + let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(2)) // Conflicting item + let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(4)) // Cloud only item let localItems: LocalContents = [localItem1, localItem2] let cloudItems: CloudContents = [cloudItem1, cloudItem3] @@ -62,10 +62,10 @@ final class SynchronizationStateManagerTests: XCTestCase { } func testInitialSynchronizationWithNewerCloudItem() { - syncStateManager = DefaultSynchronizationStateManager(isInitialSynchronization: true) + syncStateManager = iCloudSynchronizationStateResolver(isInitialSynchronization: true) let localItem = LocalMetadataItem.stub(fileName: "file", lastModificationDate: TimeInterval(1)) - let cloudItem = CloudMetadataItem.stub(fileName: "file", lastModificationDate: TimeInterval(2), isInTrash: false) + let cloudItem = CloudMetadataItem.stub(fileName: "file", lastModificationDate: TimeInterval(2)) outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringLocalContents([localItem]))) outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringCloudContents([cloudItem]))) @@ -74,10 +74,10 @@ final class SynchronizationStateManagerTests: XCTestCase { } func testInitialSynchronizationWithNewerLocalItem() { - syncStateManager = DefaultSynchronizationStateManager(isInitialSynchronization: true) + syncStateManager = iCloudSynchronizationStateResolver(isInitialSynchronization: true) let localItem = LocalMetadataItem.stub(fileName: "file", lastModificationDate: TimeInterval(2)) - let cloudItem = CloudMetadataItem.stub(fileName: "file", lastModificationDate: TimeInterval(1), isInTrash: false) + let cloudItem = CloudMetadataItem.stub(fileName: "file", lastModificationDate: TimeInterval(1)) outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringLocalContents([localItem]))) outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringCloudContents([cloudItem]))) @@ -86,10 +86,10 @@ final class SynchronizationStateManagerTests: XCTestCase { } func testInitialSynchronizationWithNonConflictingItems() { - syncStateManager = DefaultSynchronizationStateManager(isInitialSynchronization: true) + syncStateManager = iCloudSynchronizationStateResolver(isInitialSynchronization: true) let localItem = LocalMetadataItem.stub(fileName: "localFile", lastModificationDate: TimeInterval(1)) - let cloudItem = CloudMetadataItem.stub(fileName: "cloudFile", lastModificationDate: TimeInterval(2), isInTrash: false) + let cloudItem = CloudMetadataItem.stub(fileName: "cloudFile", lastModificationDate: TimeInterval(2)) outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringLocalContents([localItem]))) outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringCloudContents([cloudItem]))) @@ -134,9 +134,9 @@ final class SynchronizationStateManagerTests: XCTestCase { } func testDidFinishGatheringWhenOnlyLocalIsEmpty() { - let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false) - let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false) - let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false) + let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1)) + let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) + let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) let localItems = LocalContents() let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3]) @@ -155,25 +155,20 @@ final class SynchronizationStateManagerTests: XCTestCase { } } - func testDidFinishGatheringWhenLocalIsEmptyAndAllCloudFilesWasDeleted() { + func testDidFinishGatheringWhenTreCloudIsEmpty() { let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1)) let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) - let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(2), isInTrash: true) - let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(3), isInTrash: true) - let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(4), isInTrash: true) - let localItems = [localItem1, localItem2, localItem3] - let cloudItems = [cloudItem1, cloudItem2, cloudItem3] - outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems)) + outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents([])) outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems)) XCTAssertEqual(outgoingEvents.count, 3) outgoingEvents.forEach { event in switch event { - case .removeLocalItem(let item): + case .createCloudItem(let item): XCTAssertTrue(localItems.containsByName(item)) default: XCTFail() @@ -181,10 +176,10 @@ final class SynchronizationStateManagerTests: XCTestCase { } } - func testDidFinishGatheringWhenLocalIsNotEmptyAndAllCloudFilesWasDeleted() { - let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: true) - let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: true) - let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: true) + func testDidFinishGatheringWhenLocalIsEmpty() { + let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1)) + let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) + let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) let localItems = LocalContents() let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3]) @@ -193,40 +188,25 @@ final class SynchronizationStateManagerTests: XCTestCase { XCTAssertEqual(outgoingEvents.count, 0) outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems)) - XCTAssertEqual(outgoingEvents.count, 0) - } - - func testDidFinishGatheringWhenLocalIsEmptyAndSomeCloudFilesWasDeleted() { - let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: true) - let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: true) - let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false) - - let localItems = LocalContents() - let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3]) - - outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems)) - XCTAssertEqual(outgoingEvents.count, 0) - - outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems)) - XCTAssertEqual(outgoingEvents.count, 1) + XCTAssertEqual(outgoingEvents.count, 3) outgoingEvents.forEach { event in switch event { case .createLocalItem(let item): - XCTAssertEqual(item, cloudItem3) + XCTAssertTrue(cloudItems.containsByName(item)) default: XCTFail() } } } - func testDidFinishGatheringWhenLocalAndCloudAreNotEmptyAndEqual() { + func testDidFinishGatheringWhenLocalAndCloudAreNotEmptyAndAllFilesEqual() { let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1)) let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) - let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false) - let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false) - let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false) + let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1)) + let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) + let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) let localItems = LocalContents([localItem1, localItem2, localItem3]) let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3]) @@ -243,9 +223,9 @@ final class SynchronizationStateManagerTests: XCTestCase { let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(3)) let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(4)) - let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false) - let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false) - let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false) + let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1)) + let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) + let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) let localItems = LocalContents([localItem1, localItem2, localItem3]) let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3]) @@ -270,9 +250,9 @@ final class SynchronizationStateManagerTests: XCTestCase { let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) - let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(4), isInTrash: false) - let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false) - let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(7), isInTrash: false) + let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(4)) + let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) + let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(7)) let localItems = LocalContents([localItem1, localItem2, localItem3]) let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3]) @@ -292,93 +272,12 @@ final class SynchronizationStateManagerTests: XCTestCase { } } - func testDidFinishGatheringWhenLocalAndCloudAreNotEmptyMixed() { - let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1)) - let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(3)) - let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) - let localItem4 = LocalMetadataItem.stub(fileName: "file4", lastModificationDate: TimeInterval(1)) + func testDidFinishGatheringWhenCloudFileNewerThanLocal() { + let localItem = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) + let cloudItem = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(8)) - let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(4), isInTrash: false) - let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false) - let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(7), isInTrash: true) - - let localItems = LocalContents([localItem1, localItem2, localItem3, localItem4]) - let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3]) - - outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems)) - XCTAssertEqual(outgoingEvents.count, 0) - - outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems)) - XCTAssertEqual(outgoingEvents.count, 4) - outgoingEvents.forEach { event in - switch event { - case .updateLocalItem(let item): - XCTAssertEqual(item, cloudItem1) - case .removeLocalItem(let item): - XCTAssertEqual(item, cloudItem3) - case .createCloudItem(let item): - XCTAssertEqual(item, localItem4) - case .updateCloudItem(let item): - XCTAssertEqual(item, localItem2) - default: - XCTFail() - } - } - } - - func testDidFinishGatheringWhenCloudHaveTrashedNewerThanLocal() { - let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) - - let cloudItem3Trashed = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(8), isInTrash: true) - - let localItems = LocalContents([localItem3]) - let cloudItems = CloudContents([cloudItem3Trashed]) - - outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems)) - XCTAssertEqual(outgoingEvents.count, 0) - - outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems)) - XCTAssertEqual(outgoingEvents.count, 1) - outgoingEvents.forEach { event in - switch event { - case .removeLocalItem(let item): - XCTAssertEqual(item, cloudItem3Trashed) - default: - XCTFail() - } - } - } - - func testDidFinishGatheringWhenLocallIsEmptyAndCloudHaveSameFileBothInTrashedAndNotAndTrashedOlder() { - let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(7), isInTrash: false) - let cloudItem3Trashed = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(6), isInTrash: true) - - let localItems = LocalContents([]) - let cloudItems = CloudContents([cloudItem3, cloudItem3Trashed]) - - outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems)) - XCTAssertEqual(outgoingEvents.count, 0) - - outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems)) - XCTAssertEqual(outgoingEvents.count, 1) - outgoingEvents.forEach { event in - switch event { - case .createLocalItem(let item): - XCTAssertEqual(item, cloudItem3) - default: - XCTFail() - } - } - } - - func testDidFinishGatheringWhenCloudHaveSameFileBothInTrashedAndNotAndTrashedOlder() { - let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) - - let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(7), isInTrash: false) - let cloudItem3Trashed = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(6), isInTrash: true) - - let localItems = LocalContents([localItem3]) - let cloudItems = CloudContents([cloudItem3, cloudItem3Trashed]) + let localItems = LocalContents([localItem]) + let cloudItems = CloudContents([cloudItem]) outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems)) XCTAssertEqual(outgoingEvents.count, 0) @@ -388,7 +287,7 @@ final class SynchronizationStateManagerTests: XCTestCase { outgoingEvents.forEach { event in switch event { case .updateLocalItem(let item): - XCTAssertEqual(item, cloudItem3) + XCTAssertEqual(item, cloudItem) default: XCTFail() } @@ -398,8 +297,8 @@ final class SynchronizationStateManagerTests: XCTestCase { func testDidFinishGatheringWhenCloudHaveSameFileBothInTrashedAndNotAndTrashedBotLocalIsNewer() { let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(9)) - let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(2), isInTrash: false) - let cloudItem3Trashed = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(6), isInTrash: true) + let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(2)) + let cloudItem3Trashed = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(6)) let localItems = LocalContents([localItem3]) let cloudItems = CloudContents([cloudItem3, cloudItem3Trashed]) @@ -419,60 +318,6 @@ final class SynchronizationStateManagerTests: XCTestCase { } } - func testDidFinishGatheringWhenUpdatetLocallyItemSameAsDeletedFromCloudOnTheOtherDevice() { - let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1)) - - let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: true) - - let localItems = LocalContents([localItem1]) - let cloudItems = CloudContents([cloudItem1]) - - outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems)) - outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems)) - - var localItemsToRemove: LocalContents = [] - XCTAssertEqual(outgoingEvents.count, 1) - outgoingEvents.forEach { event in - switch event { - case .removeLocalItem(let cloudMetadataItem): - XCTAssertEqual(cloudMetadataItem, cloudItem1) - if let localItemToRemove = localItems.firstByName(cloudMetadataItem) { - localItemsToRemove.append(localItemToRemove) - } - default: - XCTFail() - } - } - - outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(localItemsToRemove)) - XCTAssertEqual(outgoingEvents.count, 0) - } - - // MARK: - Test didFinishGathering MergeConflicts - func testDidFinishGatheringMergeConflictWhenUpdatetLocallyItemNewerThanDeletedFromCloudOnTheOtherDevice() { - let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(2)) - - let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: true) - - let localItems = LocalContents([localItem1]) - let cloudItems = CloudContents([cloudItem1]) - - outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems)) - outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems)) - - // Here should be a merge conflict. New Cloud file should be created. - XCTAssertEqual(outgoingEvents.count, 1) - outgoingEvents.forEach { event in - switch event { - case .createCloudItem(let cloudMetadataItem): - XCTAssertEqual(cloudMetadataItem, localItem1) - default: - XCTFail() - } - } - } - - // MARK: - Test didUpdateLocalContents func testDidUpdateLocalContentsWhenContentWasNotChanged() { let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1)) @@ -488,9 +333,9 @@ final class SynchronizationStateManagerTests: XCTestCase { XCTAssertEqual(outgoingEvents.count, 3) let newLocalItems = LocalContents([localItem1, localItem2, localItem3]) - - outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(newLocalItems)) - XCTAssertEqual(outgoingEvents.count, 3) // Should be equal to the previous results because cloudContent wasn't changed + let update = LocalContentsUpdate(added: [], updated: [], removed: []) + outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(contents: newLocalItems, update: update)) + XCTAssertEqual(outgoingEvents.count, 0) } func testDidUpdateLocalContentsWhenNewLocalItemWasAdded() { @@ -498,9 +343,9 @@ final class SynchronizationStateManagerTests: XCTestCase { let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) - let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false) - let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false) - let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false) + let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1)) + let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) + let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) let localItems = LocalContents([localItem1, localItem2, localItem3]) let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3]) @@ -511,9 +356,9 @@ final class SynchronizationStateManagerTests: XCTestCase { XCTAssertEqual(outgoingEvents.count, 0) let localItem4 = LocalMetadataItem.stub(fileName: "file4", lastModificationDate: TimeInterval(4)) - let newLocalItems = LocalContents([localItem1, localItem2, localItem3, localItem4]) - - outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(newLocalItems)) + let newLocalItems = LocalContents([localItem1, localItem2, localItem3]) + let update = LocalContentsUpdate(added: [localItem4], updated: [], removed: []) + outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(contents: newLocalItems, update: update)) XCTAssertEqual(outgoingEvents.count, 1) outgoingEvents.forEach { event in @@ -531,9 +376,9 @@ final class SynchronizationStateManagerTests: XCTestCase { let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) - let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false) - let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false) - let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false) + let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1)) + let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) + let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) let localItems = LocalContents([localItem1, localItem2, localItem3]) let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3]) @@ -547,7 +392,8 @@ final class SynchronizationStateManagerTests: XCTestCase { let localItem3Updated = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(4)) let newLocalItems = LocalContents([localItem1, localItem2Updated, localItem3Updated]) - outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(newLocalItems)) + let update = LocalContentsUpdate(added: [], updated: [localItem2Updated, localItem3Updated], removed: []) + outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(contents: newLocalItems, update: update)) XCTAssertEqual(outgoingEvents.count, 2) outgoingEvents.forEach { event in @@ -565,9 +411,9 @@ final class SynchronizationStateManagerTests: XCTestCase { let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) - let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false) - let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false) - let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false) + let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1)) + let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) + let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) let localItems = LocalContents([localItem1, localItem2, localItem3]) let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3]) @@ -578,55 +424,14 @@ final class SynchronizationStateManagerTests: XCTestCase { XCTAssertEqual(outgoingEvents.count, 0) let newLocalItems = LocalContents([localItem1, localItem2]) - - outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(newLocalItems)) + let update = LocalContentsUpdate(added: [], updated: [], removed: [localItem3]) + outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(contents: newLocalItems, update: update)) XCTAssertEqual(outgoingEvents.count, 1) outgoingEvents.forEach { event in switch event { case .removeCloudItem(let item): - XCTAssertEqual(item, localItem3) - default: - XCTFail() - } - } - } - - func testDidUpdateLocalContentsComplexUpdate() { - let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1)) - let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) - let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) - - let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false) - let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false) - let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false) - - let localItems = LocalContents([localItem1, localItem2, localItem3]) - let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3]) - - outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems)) - outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems)) - - XCTAssertEqual(outgoingEvents.count, 0) - - let localItem1New = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(2)) - let localItem3New = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(4)) - let localItem4New = LocalMetadataItem.stub(fileName: "file4", lastModificationDate: TimeInterval(5)) - let localItem5New = LocalMetadataItem.stub(fileName: "file5", lastModificationDate: TimeInterval(5)) - - let newLocalItems = LocalContents([localItem1New, localItem3New, localItem4New, localItem5New]) - - outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(newLocalItems)) - XCTAssertEqual(outgoingEvents.count, 5) - - outgoingEvents.forEach { event in - switch event { - case .createCloudItem(let localMetadataItem): - XCTAssertTrue([localItem4New, localItem5New].containsByName(localMetadataItem)) - case .updateCloudItem(let localMetadataItem): - XCTAssertTrue([localItem1New, localItem3New].containsByName(localMetadataItem)) - case .removeCloudItem(let localMetadataItem): - XCTAssertEqual(localMetadataItem, localItem2) + XCTAssertEqual(item, cloudItem3) default: XCTFail() } @@ -639,9 +444,9 @@ final class SynchronizationStateManagerTests: XCTestCase { let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) - let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false) - let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false) - let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false) + let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1)) + let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) + let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) let localItems = LocalContents([localItem1, localItem2, localItem3]) let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3]) @@ -652,8 +457,8 @@ final class SynchronizationStateManagerTests: XCTestCase { XCTAssertEqual(outgoingEvents.count, 0) let newCloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3]) - - outgoingEvents = syncStateManager.resolveEvent(.didUpdateCloudContents(newCloudItems)) + let update = CloudContentsUpdate(added: [], updated: [], removed: []) + outgoingEvents = syncStateManager.resolveEvent(.didUpdateCloudContents(contents: newCloudItems, update: update)) XCTAssertEqual(outgoingEvents.count, 0) } @@ -662,9 +467,9 @@ final class SynchronizationStateManagerTests: XCTestCase { let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) - let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false) - let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false) - let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false) + let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1)) + let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) + let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) let localItems = LocalContents([localItem1, localItem2, localItem3]) var cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3]) @@ -674,10 +479,10 @@ final class SynchronizationStateManagerTests: XCTestCase { XCTAssertEqual(outgoingEvents.count, 0) - var cloudItem4 = CloudMetadataItem.stub(fileName: "file4", lastModificationDate: TimeInterval(3), isInTrash: false, isDownloaded: false) + var cloudItem4 = CloudMetadataItem.stub(fileName: "file4", lastModificationDate: TimeInterval(3), isDownloaded: false) cloudItems.append(cloudItem4) - - outgoingEvents = syncStateManager.resolveEvent(.didUpdateCloudContents(cloudItems)) + var update = CloudContentsUpdate(added: [cloudItem4], updated: [], removed: []) + outgoingEvents = syncStateManager.resolveEvent(.didUpdateCloudContents(contents: cloudItems, update: update)) XCTAssertEqual(outgoingEvents.count, 1) outgoingEvents.forEach { event in switch event { @@ -692,7 +497,8 @@ final class SynchronizationStateManagerTests: XCTestCase { // recreate collection cloudItems = [cloudItem1, cloudItem2, cloudItem3, cloudItem4] - outgoingEvents = syncStateManager.resolveEvent(.didUpdateCloudContents(cloudItems)) + update = CloudContentsUpdate(added: [], updated: [cloudItem4], removed: []) + outgoingEvents = syncStateManager.resolveEvent(.didUpdateCloudContents(contents: cloudItems, update: update)) XCTAssertEqual(outgoingEvents.count, 1) outgoingEvents.forEach { event in @@ -704,49 +510,5 @@ final class SynchronizationStateManagerTests: XCTestCase { } } } - - func testDidUpdateCloudContentsWhenAllContentWasTrashed() { - let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1)) - let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2)) - let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3)) - - var cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false) - var cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false) - var cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false) - - let localItems = LocalContents([localItem1, localItem2, localItem3]) - var cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3]) - - outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems)) - outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems)) - - XCTAssertEqual(outgoingEvents.count, 0) - - cloudItem1.isRemoved = true - cloudItem2.isRemoved = true - cloudItem3.isRemoved = true - - cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3]) - - outgoingEvents = syncStateManager.resolveEvent(.didUpdateCloudContents(cloudItems)) - XCTAssertEqual(outgoingEvents.count, 3) - - var localItemsToRemove: LocalContents = [] - outgoingEvents.forEach { event in - switch event { - case .removeLocalItem(let cloudMetadataItem): - XCTAssertTrue(cloudItems.containsByName(cloudMetadataItem)) - if let localItemToRemove = localItems.firstByName(cloudMetadataItem) { - localItemsToRemove.append(localItemToRemove) - } - default: - XCTFail() - } - } - - outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(localItemsToRemove)) - // Because all cloud items in .trash and we have removed all local items, we should not have any outgoing events. - XCTAssertEqual(outgoingEvents.count, 0) - } } diff --git a/iphone/Maps/Tests/Core/iCloudTests/iCloudDirectoryMonitorTests/UbiquitousDirectoryMonitorDelegateMock.swift b/iphone/Maps/Tests/Core/iCloudTests/iCloudDirectoryMonitorTests/UbiquitousDirectoryMonitorDelegateMock.swift index ddfb386162..d05f466638 100644 --- a/iphone/Maps/Tests/Core/iCloudTests/iCloudDirectoryMonitorTests/UbiquitousDirectoryMonitorDelegateMock.swift +++ b/iphone/Maps/Tests/Core/iCloudTests/iCloudDirectoryMonitorTests/UbiquitousDirectoryMonitorDelegateMock.swift @@ -12,13 +12,13 @@ class UbiquitousDirectoryMonitorDelegateMock: CloudDirectoryMonitorDelegate { var contents = CloudContents() - func didFinishGathering(contents: CloudContents) { + func didFinishGathering(_ contents: CloudContents) { didFinishGatheringCalled = true didFinishGatheringExpectation?.fulfill() self.contents = contents } - func didUpdate(contents: CloudContents) { + func didUpdate(_ contents: CloudContents, _ update: CloudContentsUpdate) { didUpdateCalled = true didUpdateExpectation?.fulfill() self.contents = contents diff --git a/iphone/Maps/Tests/Core/iCloudTests/iCloudDirectoryMonitorTests/iCloudDirectoryMonitorTests.swift b/iphone/Maps/Tests/Core/iCloudTests/iCloudDirectoryMonitorTests/iCloudDirectoryMonitorTests.swift index 692801c2e6..3bd6fc6bae 100644 --- a/iphone/Maps/Tests/Core/iCloudTests/iCloudDirectoryMonitorTests/iCloudDirectoryMonitorTests.swift +++ b/iphone/Maps/Tests/Core/iCloudTests/iCloudDirectoryMonitorTests/iCloudDirectoryMonitorTests.swift @@ -5,7 +5,7 @@ typealias UbiquityIdentityToken = NSCoding & NSCopying & NSObjectProtocol class iCloudDirectoryMonitorTests: XCTestCase { - var cloudMonitor: iCloudDocumentsDirectoryMonitor! + var cloudMonitor: iCloudDocumentsMonitor! var mockFileManager: FileManagerMock! var mockDelegate: UbiquitousDirectoryMonitorDelegateMock! var cloudContainerIdentifier: String = "iCloud.app.organicmaps.debug" @@ -14,7 +14,7 @@ class iCloudDirectoryMonitorTests: XCTestCase { super.setUp() mockFileManager = FileManagerMock() mockDelegate = UbiquitousDirectoryMonitorDelegateMock() - cloudMonitor = iCloudDocumentsDirectoryMonitor(fileManager: mockFileManager, cloudContainerIdentifier: cloudContainerIdentifier, fileType: .kml) + cloudMonitor = iCloudDocumentsMonitor(fileManager: mockFileManager, cloudContainerIdentifier: cloudContainerIdentifier, fileType: .kml) cloudMonitor.delegate = mockDelegate } diff --git a/iphone/Maps/UI/Settings/Cells/SettingsTableViewiCloudSwitchCell.swift b/iphone/Maps/UI/Settings/Cells/SettingsTableViewiCloudSwitchCell.swift index c25efc9824..d429735a1b 100644 --- a/iphone/Maps/UI/Settings/Cells/SettingsTableViewiCloudSwitchCell.swift +++ b/iphone/Maps/UI/Settings/Cells/SettingsTableViewiCloudSwitchCell.swift @@ -1,7 +1,7 @@ final class SettingsTableViewiCloudSwitchCell: SettingsTableViewDetailedSwitchCell { @objc - func updateWithSynchronizationState(_ state: CloudStorageSynchronizationState) { + func updateWithSynchronizationState(_ state: SynchronizationManagerState) { guard state.isAvailable else { accessoryView = nil accessoryType = .detailButton diff --git a/iphone/Maps/UI/Settings/MWMSettingsViewController.mm b/iphone/Maps/UI/Settings/MWMSettingsViewController.mm index eb1297f806..d4cf08a266 100644 --- a/iphone/Maps/UI/Settings/MWMSettingsViewController.mm +++ b/iphone/Maps/UI/Settings/MWMSettingsViewController.mm @@ -188,7 +188,7 @@ static NSString * const kUDDidShowICloudSynchronizationEnablingAlert = @"kUDDidS isOn:[MWMSettings iCLoudSynchronizationEnabled]]; __weak __typeof(self) weakSelf = self; - [CloudStorageManager.shared addObserver:self synchronizationStateDidChangeHandler:^(CloudStorageSynchronizationState * state) { + [iCloudSynchronizaionManager.shared addObserver:self synchronizationStateDidChangeHandler:^(SynchronizationManagerState * state) { __strong auto strongSelf = weakSelf; [strongSelf.iCloudSynchronizationCell updateWithSynchronizationState:state]; }];