From 286fd5596913b01e07dd20299b7e45e21e53f2a8 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 26 May 2023 12:11:59 +0200 Subject: [PATCH] Sync Engine with support for syncing bookmarks (#1724) Task/Issue URL: https://app.asana.com/0/0/1204606874917765/f Description: Add support for syncing bookmarks --- .swiftlint.yml | 1 + Core/BookmarksCachingSearch.swift | 10 +- Core/BookmarksCleanupErrorHandling.swift | 40 +++++++ Core/BookmarksExporter.swift | 5 +- Core/Logging.swift | 5 +- Core/PixelEvent.swift | 12 +- Core/SyncBookmarksAdapter.swift | 75 ++++++++++++ Core/SyncDataProviders.swift | 70 +++++++++++ Core/SyncMetadataDatabase.swift | 54 +++++++++ DuckDuckGo.xcodeproj/project.pbxproj | 50 ++++++-- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../AddOrEditBookmarkViewController.swift | 1 + DuckDuckGo/AppDelegate.swift | 29 +++-- DuckDuckGo/AppDependencyProvider.swift | 2 + DuckDuckGo/BookmarksDatabase.swift | 1 - DuckDuckGo/BookmarksViewController.swift | 24 +++- DuckDuckGo/FavoritesViewController.swift | 23 +++- DuckDuckGo/FireproofFaviconUpdater.swift | 2 + DuckDuckGo/MainViewController.swift | 63 ++++++++-- DuckDuckGo/RemoteMessaging.swift | 14 ++- DuckDuckGo/SyncDataPersistor.swift | 32 ----- DuckDuckGo/SyncSettingsViewController.swift | 10 +- ...bViewControllerBrowsingMenuExtension.swift | 1 + ...ControllerLongPressBookmarkExtension.swift | 5 +- .../BookmarkEditorViewModelTests.swift | 8 +- DuckDuckGoTests/BookmarkEntityTests.swift | 32 ----- .../BookmarkListViewModelTests.swift | 6 +- DuckDuckGoTests/BookmarksImporterTests.swift | 4 +- DuckDuckGoTests/BookmarksIndexesTests.swift | 112 ++++++++++++++++++ DuckDuckGoTests/MockDependencyProvider.swift | 1 + .../SyncUI/Views/SyncSettingsView.swift | 2 +- .../BookmarksLookupPerformanceTests.swift | 15 ++- 32 files changed, 584 insertions(+), 129 deletions(-) create mode 100644 Core/BookmarksCleanupErrorHandling.swift create mode 100644 Core/SyncBookmarksAdapter.swift create mode 100644 Core/SyncDataProviders.swift create mode 100644 Core/SyncMetadataDatabase.swift delete mode 100644 DuckDuckGo/SyncDataPersistor.swift create mode 100644 DuckDuckGoTests/BookmarksIndexesTests.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 572f96e47d..c03bdcfb4f 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -86,6 +86,7 @@ file_header: # General Config excluded: - Carthage + - DerivedData - DuckDuckGo/UserText.swift - fastlane/SnapshotHelper.swift - vendor diff --git a/Core/BookmarksCachingSearch.swift b/Core/BookmarksCachingSearch.swift index d117fb119d..17b264b198 100644 --- a/Core/BookmarksCachingSearch.swift +++ b/Core/BookmarksCachingSearch.swift @@ -62,11 +62,15 @@ public class CoreDataBookmarksSearchStore: BookmarksSearchStore { let context = bookmarksStore.makeContext(concurrencyType: .privateQueueConcurrencyType) let fetchRequest = NSFetchRequest(entityName: "BookmarkEntity") - fetchRequest.predicate = NSPredicate(format: "%K = false", #keyPath(BookmarkEntity.isFolder)) + fetchRequest.predicate = NSPredicate( + format: "%K = false AND %K == NO", + #keyPath(BookmarkEntity.isFolder), + #keyPath(BookmarkEntity.isPendingDeletion) + ) fetchRequest.resultType = .dictionaryResultType fetchRequest.propertiesToFetch = [#keyPath(BookmarkEntity.title), #keyPath(BookmarkEntity.url), - #keyPath(BookmarkEntity.isFavorite), + #keyPath(BookmarkEntity.favoriteFolder), #keyPath(BookmarkEntity.objectID)] context.perform { @@ -127,7 +131,7 @@ public class BookmarksCachingSearch: BookmarksStringSearch { self.init(objectID: objectID, title: title, url: url, - isFavorite: (bookmark[#keyPath(BookmarkEntity.isFavorite)] as? NSNumber)?.boolValue ?? false) + isFavorite: bookmark[#keyPath(BookmarkEntity.favoriteFolder)] != nil) } public func togglingFavorite() -> BookmarksStringSearchResult { diff --git a/Core/BookmarksCleanupErrorHandling.swift b/Core/BookmarksCleanupErrorHandling.swift new file mode 100644 index 0000000000..48c035d407 --- /dev/null +++ b/Core/BookmarksCleanupErrorHandling.swift @@ -0,0 +1,40 @@ +// +// BookmarksCleanupErrorHandling.swift +// DuckDuckGo +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Bookmarks +import Common +import Persistence + +public class BookmarksCleanupErrorHandling: EventMapping { + + public init() { + super.init { event, _, _, _ in + let domainEvent = Pixel.Event.bookmarksCleanupFailed + let processedErrors = CoreDataErrorsParser.parse(error: event.coreDataError as NSError) + let params = processedErrors.errorPixelParameters + + Pixel.fire(pixel: domainEvent, error: event.coreDataError, withAdditionalParameters: params) + } + } + + override init(mapping: @escaping EventMapping.Mapping) { + fatalError("Use init()") + } +} diff --git a/Core/BookmarksExporter.swift b/Core/BookmarksExporter.swift index ca46e31704..53fb563af9 100644 --- a/Core/BookmarksExporter.swift +++ b/Core/BookmarksExporter.swift @@ -45,8 +45,9 @@ public struct BookmarksExporter { guard let rootFolder = BookmarkUtils.fetchRootFolder(context) else { throw BookmarksExporterError.brokenDatabaseStructure } - - let topLevelBookmarksAndFavorites = rootFolder.childrenArray + + let orphanedBookmarks = BookmarkUtils.fetchOrphanedEntities(context) + let topLevelBookmarksAndFavorites = rootFolder.childrenArray + orphanedBookmarks content.append(contentsOf: export(topLevelBookmarksAndFavorites, level: 2)) content.append(Template.footer) return content.joined() diff --git a/Core/Logging.swift b/Core/Logging.swift index 9fa70fba0e..fd6bdac037 100644 --- a/Core/Logging.swift +++ b/Core/Logging.swift @@ -30,6 +30,7 @@ public extension OSLog { case lifecycleLog = "DDG Lifecycle" case autoconsentLog = "DDG Autoconsent" case configurationLog = "DDG Configuration" + case syncLog = "DDG Sync" } @OSLogWrapper(.generalLog) static var generalLog @@ -38,6 +39,7 @@ public extension OSLog { @OSLogWrapper(.lifecycleLog) static var lifecycleLog @OSLogWrapper(.autoconsentLog) static var autoconsentLog @OSLogWrapper(.configurationLog) static var configurationLog + @OSLogWrapper(.syncLog) static var syncLog // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // To activate Logging Categories add categories here: @@ -47,7 +49,8 @@ public extension OSLog { .contentBlockingLog, .adAttributionLog, .lifecycleLog, - .configurationLog + .configurationLog, + .syncLog ] #endif diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 87391e08d4..15d25db2f1 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -401,13 +401,18 @@ extension Pixel { case bookmarksCouldNotLoadDatabase case bookmarksCouldNotPrepareDatabase + case bookmarksCleanupFailed case bookmarksMigrationAlreadyPerformed case bookmarksMigrationFailed case bookmarksMigrationCouldNotPrepareDatabase case bookmarksMigrationCouldNotPrepareDatabaseOnFailedMigration case bookmarksMigrationCouldNotValidateDatabase case bookmarksMigrationCouldNotRemoveOldStore - + + case syncMetadataCouldNotLoadDatabase + case syncBookmarksProviderInitializationFailed + case syncBookmarksFailed + case invalidPayload(Configuration) } @@ -797,6 +802,7 @@ extension Pixel.Event { case .bookmarksCouldNotLoadDatabase: return "m_d_bookmarks_could_not_load_database" case .bookmarksCouldNotPrepareDatabase: return "m_d_bookmarks_could_not_prepare_database" + case .bookmarksCleanupFailed: return "m_d_bookmarks_cleanup_failed" case .bookmarksMigrationAlreadyPerformed: return "m_d_bookmarks_migration_already_performed" case .bookmarksMigrationFailed: return "m_d_bookmarks_migration_failed" case .bookmarksMigrationCouldNotPrepareDatabase: return "m_d_bookmarks_migration_could_not_prepare_database" @@ -805,6 +811,10 @@ extension Pixel.Event { case .bookmarksMigrationCouldNotValidateDatabase: return "m_d_bookmarks_migration_could_not_validate_database" case .bookmarksMigrationCouldNotRemoveOldStore: return "m_d_bookmarks_migration_could_not_remove_old_store" + case .syncMetadataCouldNotLoadDatabase: return "m_d_sync_metadata_could_not_load_database" + case .syncBookmarksProviderInitializationFailed: return "m_d_sync_bookmarks_provider_initialization_failed" + case .syncBookmarksFailed: return "m_d_sync_bookmarks_failed" + case .invalidPayload(let configuration): return "m_d_\(configuration.rawValue)_invalid_payload".lowercased() } diff --git a/Core/SyncBookmarksAdapter.swift b/Core/SyncBookmarksAdapter.swift new file mode 100644 index 0000000000..cf5be5288e --- /dev/null +++ b/Core/SyncBookmarksAdapter.swift @@ -0,0 +1,75 @@ +// +// SyncBookmarksAdapter.swift +// DuckDuckGo +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Common +import DDGSync +import Foundation +import Persistence +import SyncDataProviders + +public final class SyncBookmarksAdapter { + + public private(set) var provider: BookmarksProvider? + + public let syncDidCompletePublisher: AnyPublisher + + public init() { + syncDidCompletePublisher = syncDidCompleteSubject.eraseToAnyPublisher() + } + + public func setUpProviderIfNeeded(database: CoreDataDatabase, metadataStore: SyncMetadataStore) { + guard provider == nil else { + return + } + do { + let provider = try BookmarksProvider( + database: database, + metadataStore: metadataStore, + reloadBookmarksAfterSync: { [syncDidCompleteSubject] in + syncDidCompleteSubject.send() + } + ) + + syncErrorCancellable = provider.syncErrorPublisher + .sink { error in + switch error { + case let syncError as SyncError: + Pixel.fire(pixel: .syncBookmarksFailed, error: syncError) + default: + let nsError = error as NSError + if nsError.domain != NSURLErrorDomain { + let processedErrors = CoreDataErrorsParser.parse(error: error as NSError) + let params = processedErrors.errorPixelParameters + Pixel.fire(pixel: .syncBookmarksFailed, error: error, withAdditionalParameters: params) + } + } + os_log(.error, log: OSLog.syncLog, "Bookmarks Sync error: %{public}s", String(reflecting: error)) + } + self.provider = provider + } catch let error as NSError { + let processedErrors = CoreDataErrorsParser.parse(error: error) + let params = processedErrors.errorPixelParameters + Pixel.fire(pixel: .syncBookmarksProviderInitializationFailed, error: error, withAdditionalParameters: params) + } + } + + private var syncDidCompleteSubject = PassthroughSubject() + private var syncErrorCancellable: AnyCancellable? +} diff --git a/Core/SyncDataProviders.swift b/Core/SyncDataProviders.swift new file mode 100644 index 0000000000..d09b53d00a --- /dev/null +++ b/Core/SyncDataProviders.swift @@ -0,0 +1,70 @@ +// +// SyncDataProviders.swift +// DuckDuckGo +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Common +import DDGSync +import Persistence +import SyncDataProviders + +public class SyncDataProviders: DataProvidersSource { + public let bookmarksAdapter: SyncBookmarksAdapter + + public func makeDataProviders() -> [DataProviding] { + initializeMetadataDatabaseIfNeeded() + guard let syncMetadata else { + assertionFailure("Sync Metadata not initialized") + return [] + } + + bookmarksAdapter.setUpProviderIfNeeded(database: bookmarksDatabase, metadataStore: syncMetadata) + return [bookmarksAdapter.provider].compactMap { $0 } + } + + public init(bookmarksDatabase: CoreDataDatabase) { + self.bookmarksDatabase = bookmarksDatabase + bookmarksAdapter = SyncBookmarksAdapter() + } + + private func initializeMetadataDatabaseIfNeeded() { + guard !isSyncMetadaDatabaseLoaded else { + return + } + + syncMetadataDatabase.loadStore { context, error in + guard context != nil else { + if let error = error { + Pixel.fire(pixel: .syncMetadataCouldNotLoadDatabase, error: error) + } else { + Pixel.fire(pixel: .syncMetadataCouldNotLoadDatabase) + } + + Thread.sleep(forTimeInterval: 1) + fatalError("Could not create Sync Metadata database stack: \(error?.localizedDescription ?? "err")") + } + } + syncMetadata = LocalSyncMetadataStore(database: syncMetadataDatabase) + isSyncMetadaDatabaseLoaded = true + } + + private var isSyncMetadaDatabaseLoaded: Bool = false + private var syncMetadata: SyncMetadataStore? + + private let syncMetadataDatabase: CoreDataDatabase = SyncMetadataDatabase.make() + private let bookmarksDatabase: CoreDataDatabase +} diff --git a/Core/SyncMetadataDatabase.swift b/Core/SyncMetadataDatabase.swift new file mode 100644 index 0000000000..a714176eab --- /dev/null +++ b/Core/SyncMetadataDatabase.swift @@ -0,0 +1,54 @@ +// +// SyncMetadataDatabase.swift +// DuckDuckGo +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import CoreData +import DDGSync +import Persistence +import Common + +public final class SyncMetadataDatabase { + + private init() { } + + public static var defaultDBLocation: URL = { + guard let url = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else { + os_log("SyncMetadataDatabase.make - OUT, failed to get location") + fatalError("Failed to get location") + } + return url + }() + + public static func make(location: URL = defaultDBLocation, readOnly: Bool = false) -> CoreDataDatabase { + os_log("SyncMetadataDatabase.make - IN - %s", location.absoluteString) + let bundle = DDGSync.bundle + guard let model = CoreDataDatabase.loadModel(from: bundle, named: "SyncMetadata") else { + os_log("SyncMetadataDatabase.make - OUT, failed to loadModel") + fatalError("Failed to load model") + } + + let db = CoreDataDatabase(name: "SyncMetadata", + containerLocation: location, + model: model, + readOnly: readOnly) + os_log("SyncMetadataDatabase.make - OUT") + return db + } + +} diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 5571bf8d6f..e85de81b82 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -239,7 +239,13 @@ 31DD208427395A5A008FB313 /* VoiceSearchHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31DD208327395A5A008FB313 /* VoiceSearchHelper.swift */; }; 31E69A63280F4CB600478327 /* DuckUI in Frameworks */ = {isa = PBXBuildFile; productRef = 31E69A62280F4CB600478327 /* DuckUI */; }; 31EF52E1281B3BDC0034796E /* AutofillLoginListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31EF52E0281B3BDC0034796E /* AutofillLoginListItemViewModel.swift */; }; + 373D9B4529ED95C800381FDD /* BookmarksIndexesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373D9B4429ED95C800381FDD /* BookmarksIndexesTests.swift */; }; + 37445F972A155F7C0029F789 /* SyncDataProviders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37445F962A155F7C0029F789 /* SyncDataProviders.swift */; }; 3760DFED299315EF0045A446 /* Waitlist in Frameworks */ = {isa = PBXBuildFile; productRef = 3760DFEC299315EF0045A446 /* Waitlist */; }; + 379E877429E97C8D001C8BB0 /* BookmarksCleanupErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379E877329E97C8D001C8BB0 /* BookmarksCleanupErrorHandling.swift */; }; + 37DF000A29F9C416002B7D3E /* SyncMetadataDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DF000929F9C416002B7D3E /* SyncMetadataDatabase.swift */; }; + 37DF000C29F9CA80002B7D3E /* SyncDataProviders in Frameworks */ = {isa = PBXBuildFile; productRef = 37DF000B29F9CA80002B7D3E /* SyncDataProviders */; }; + 37DF000F29F9D635002B7D3E /* SyncBookmarksAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DF000E29F9D635002B7D3E /* SyncBookmarksAdapter.swift */; }; 37FCAAAB29911BF1000E420A /* WaitlistExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FCAAAA29911BF1000E420A /* WaitlistExtensions.swift */; }; 37FCAAB229914232000E420A /* WindowsBrowserWaitlistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FCAAB129914232000E420A /* WindowsBrowserWaitlistView.swift */; }; 37FCAAB429914C77000E420A /* WindowsWaitlistViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FCAAB329914C77000E420A /* WindowsWaitlistViewController.swift */; }; @@ -388,7 +394,6 @@ 8565A34B1FC8D96B00239327 /* LaunchTabNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8565A34A1FC8D96B00239327 /* LaunchTabNotification.swift */; }; 8565A34D1FC8DFE400239327 /* LaunchTabNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8565A34C1FC8DFE400239327 /* LaunchTabNotificationTests.swift */; }; 8577A1C5255D2C0D00D43FCD /* HitTestingToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8577A1C4255D2C0D00D43FCD /* HitTestingToolbar.swift */; }; - 857C896D29C88E5300297DBE /* SyncDataPersistor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 857C896C29C88E5300297DBE /* SyncDataPersistor.swift */; }; 857EEB752095FFAC008A005C /* HomeRowInstructionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 857EEB742095FFAC008A005C /* HomeRowInstructionsViewController.swift */; }; 858566E8252E4F56007501B8 /* Debug.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 858566E7252E4F56007501B8 /* Debug.storyboard */; }; 858566FB252E55D6007501B8 /* ImageCacheDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 858566FA252E55D6007501B8 /* ImageCacheDebugViewController.swift */; }; @@ -1178,6 +1183,11 @@ 31CC224828369B38001654A4 /* AutofillLoginSettingsListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginSettingsListViewController.swift; sourceTree = ""; }; 31DD208327395A5A008FB313 /* VoiceSearchHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceSearchHelper.swift; sourceTree = ""; }; 31EF52E0281B3BDC0034796E /* AutofillLoginListItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginListItemViewModel.swift; sourceTree = ""; }; + 373D9B4429ED95C800381FDD /* BookmarksIndexesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksIndexesTests.swift; sourceTree = ""; }; + 37445F962A155F7C0029F789 /* SyncDataProviders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncDataProviders.swift; sourceTree = ""; }; + 379E877329E97C8D001C8BB0 /* BookmarksCleanupErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksCleanupErrorHandling.swift; sourceTree = ""; }; + 37DF000929F9C416002B7D3E /* SyncMetadataDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncMetadataDatabase.swift; sourceTree = ""; }; + 37DF000E29F9D635002B7D3E /* SyncBookmarksAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncBookmarksAdapter.swift; sourceTree = ""; }; 37FCAAAA29911BF1000E420A /* WaitlistExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistExtensions.swift; sourceTree = ""; }; 37FCAAB129914232000E420A /* WindowsBrowserWaitlistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowsBrowserWaitlistView.swift; sourceTree = ""; }; 37FCAAB329914C77000E420A /* WindowsWaitlistViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowsWaitlistViewController.swift; sourceTree = ""; }; @@ -1326,7 +1336,6 @@ 8565A34A1FC8D96B00239327 /* LaunchTabNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchTabNotification.swift; sourceTree = ""; }; 8565A34C1FC8DFE400239327 /* LaunchTabNotificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchTabNotificationTests.swift; sourceTree = ""; }; 8577A1C4255D2C0D00D43FCD /* HitTestingToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTestingToolbar.swift; sourceTree = ""; }; - 857C896C29C88E5300297DBE /* SyncDataPersistor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncDataPersistor.swift; sourceTree = ""; }; 857EEB742095FFAC008A005C /* HomeRowInstructionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeRowInstructionsViewController.swift; sourceTree = ""; }; 858566E7252E4F56007501B8 /* Debug.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Debug.storyboard; sourceTree = ""; }; 858566FA252E55D6007501B8 /* ImageCacheDebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCacheDebugViewController.swift; sourceTree = ""; }; @@ -2457,6 +2466,7 @@ 98A16C2D28A11D6200A6C003 /* BrowserServicesKit in Frameworks */, 8599690F29D2F1C100DBF9FA /* DDGSync in Frameworks */, 1E60989F290011E600A508F9 /* PrivacyDashboard in Frameworks */, + 37DF000C29F9CA80002B7D3E /* SyncDataProviders in Frameworks */, 1E6098A1290011E600A508F9 /* UserScript in Frameworks */, C14882ED27F211A000D59F0C /* SwiftSoup in Frameworks */, ); @@ -3138,6 +3148,16 @@ path = LocalPackages; sourceTree = ""; }; + 37DF000829F9C3F0002B7D3E /* Sync */ = { + isa = PBXGroup; + children = ( + 37DF000929F9C416002B7D3E /* SyncMetadataDatabase.swift */, + 37DF000E29F9D635002B7D3E /* SyncBookmarksAdapter.swift */, + 37445F962A155F7C0029F789 /* SyncDataProviders.swift */, + ); + name = Sync; + sourceTree = ""; + }; 37FCAAA0299117F9000E420A /* MacBrowser */ = { isa = PBXGroup; children = ( @@ -3734,14 +3754,6 @@ name = AutoComplete; sourceTree = ""; }; - 85E592BE2986C10E0070419D /* Service */ = { - isa = PBXGroup; - children = ( - 857C896C29C88E5300297DBE /* SyncDataPersistor.swift */, - ); - name = Service; - sourceTree = ""; - }; 85EE7F53224667C3000FE757 /* WebContainer */ = { isa = PBXGroup; children = ( @@ -3777,7 +3789,6 @@ 85F98F8C296F0ED100742F4A /* Sync */ = { isa = PBXGroup; children = ( - 85E592BE2986C10E0070419D /* Service */, 85F98F97296F4CB100742F4A /* SyncAssets.xcassets */, 85F0E97229952D7A003D5181 /* DuckDuckGo Recovery Document.pdf */, 85DD44232976C7A8005CC388 /* Controllers */, @@ -3856,6 +3867,7 @@ 987130BD294AAB8200AB05E0 /* BSK */ = { isa = PBXGroup; children = ( + 373D9B4429ED95C800381FDD /* BookmarksIndexesTests.swift */, 986B45CF299E30A50089D2D7 /* BookmarkEntityTests.swift */, 987130BF294AAB9E00AB05E0 /* BookmarkEditorViewModelTests.swift */, 987130C0294AAB9E00AB05E0 /* BookmarkListViewModelTests.swift */, @@ -4395,6 +4407,7 @@ 98B001AE251EABB40090EC07 /* InfoPlist.strings */, F18608DE1E5E648100361C30 /* Javascript */, F1134EA71F3E2B3500B73467 /* Statistics */, + 37DF000829F9C3F0002B7D3E /* Sync */, F143C3191E4A99DD00CFDE3A /* Utilities */, F143C3311E4A9A6A00CFDE3A /* Web */, CBAA195B27C3982A00A4BD49 /* PrivacyFeatures.swift */, @@ -4718,6 +4731,7 @@ children = ( 8501186529001D6900BDEE27 /* BookmarksDatabase.swift */, 9856A1982933D2EB00ACB44F /* BookmarksModelsErrorHandling.swift */, + 379E877329E97C8D001C8BB0 /* BookmarksCleanupErrorHandling.swift */, C14882D627F2010700D59F0C /* ImportExport */, F1CE42A81ECA0A660074A8DF /* LegacyStore */, ); @@ -5153,6 +5167,7 @@ CBC83E3329B631780008E19C /* Configuration */, 8599690E29D2F1C100DBF9FA /* DDGSync */, 4B948E2529DCCDB9002531FA /* Persistence */, + 37DF000B29F9CA80002B7D3E /* SyncDataProviders */, ); productName = Core; productReference = F143C2E41E4A4CD400CFDE3A /* Core.framework */; @@ -5789,7 +5804,6 @@ 02A54A9E2A097F0F000C8FED /* AppTPCollectionViewCell.swift in Sources */, C1B7B529289420830098FD6A /* RemoteMessaging.xcdatamodeld in Sources */, 986B16C425E92DF0007D23E8 /* BrowsingMenuViewController.swift in Sources */, - 857C896D29C88E5300297DBE /* SyncDataPersistor.swift in Sources */, 988AC355257E47C100793C64 /* RequeryLogic.swift in Sources */, 1E4F4A5A297193DE00625985 /* MainViewController+CookiesManaged.swift in Sources */, 8586A10D24CBA7070049720E /* FindInPageActivity.swift in Sources */, @@ -6182,6 +6196,7 @@ B6AD9E3728D4510A0019CDE9 /* ContentBlockingUpdatingTests.swift in Sources */, C14882E427F20D9A00D59F0C /* BookmarksImporterTests.swift in Sources */, 8588026A24E424EE00C24AB6 /* AppWidthObserverTests.swift in Sources */, + 373D9B4529ED95C800381FDD /* BookmarksIndexesTests.swift in Sources */, 8588026624E420BD00C24AB6 /* LargeOmniBarStateTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -6278,6 +6293,7 @@ 02CA904B24F6C11A00D41DDF /* NavigatorSharePatchUserScript.swift in Sources */, 85BDC3192436161C0053DB07 /* LoginFormDetectionUserScript.swift in Sources */, 98982B3422F8D8E400578AC9 /* Debounce.swift in Sources */, + 37DF000A29F9C416002B7D3E /* SyncMetadataDatabase.swift in Sources */, F143C3291E4A9A0E00CFDE3A /* URLExtension.swift in Sources */, F143C3271E4A9A0E00CFDE3A /* Logging.swift in Sources */, 4B83396C29AC0701003F7EA9 /* AppTrackingProtectionStoringModel.swift in Sources */, @@ -6291,6 +6307,7 @@ F1134EB51F40AEEA00B73467 /* StatisticsLoader.swift in Sources */, CB2A7EF4285383B300885F67 /* AppLastCompiledRulesStore.swift in Sources */, 4B75EA9226A266CB00018634 /* PrintingUserScript.swift in Sources */, + 37445F972A155F7C0029F789 /* SyncDataProviders.swift in Sources */, CB258D1F29A52B2500DEBA24 /* Configuration.swift in Sources */, 9847C00027A2DDBB00DB07AA /* AppPrivacyConfigurationDataProvider.swift in Sources */, F143C3281E4A9A0E00CFDE3A /* StringExtension.swift in Sources */, @@ -6306,6 +6323,7 @@ 98F6EA472863124100720957 /* ContentBlockerRulesLists.swift in Sources */, F1134EB01F40AC6300B73467 /* AtbParser.swift in Sources */, EE50052E29C369D300AE0773 /* FeatureFlag.swift in Sources */, + 37DF000F29F9D635002B7D3E /* SyncBookmarksAdapter.swift in Sources */, B652DF10287C2C1600C12A9C /* ContentBlocking.swift in Sources */, 4BE2756827304F57006B20B0 /* URLRequestExtension.swift in Sources */, 85BA79911F6FF75000F59015 /* ContentBlockerStoreConstants.swift in Sources */, @@ -6330,6 +6348,7 @@ F1D477CB1F2149C40031ED49 /* Type.swift in Sources */, 1E05D1D629C46EBB00BF9A1F /* DailyPixel.swift in Sources */, 1CB7B82123CEA1F800AA24EA /* DateExtension.swift in Sources */, + 379E877429E97C8D001C8BB0 /* BookmarksCleanupErrorHandling.swift in Sources */, 988F3DCF237D5C0F00AEE34C /* SchemeHandler.swift in Sources */, 9875E00722316B8400B1373F /* Instruments.swift in Sources */, F1134EA61F3E2AF400B73467 /* StatisticsStore.swift in Sources */, @@ -8152,7 +8171,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 57.7.0; + version = 58.0.0; }; }; C14882EB27F211A000D59F0C /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { @@ -8254,6 +8273,11 @@ isa = XCSwiftPackageProductDependency; productName = Waitlist; }; + 37DF000B29F9CA80002B7D3E /* SyncDataProviders */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = SyncDataProviders; + }; 4B2754EB29E8C7DF00394032 /* Lottie */ = { isa = XCSwiftPackageProductDependency; package = 4B2754EA29E8C7DF00394032 /* XCRemoteSwiftPackageReference "lottie-ios" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 662e4ff04f..9b075534a3 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/DuckDuckGo/BrowserServicesKit", "state": { "branch": null, - "revision": "34746649bc3ef115da7bfd46ca41013ecb1b995c", - "version": "57.7.0" + "revision": "52aed5b9cb67f73558da72b09b492c4c98ae3f37", + "version": "58.0.0" } }, { diff --git a/DuckDuckGo/AddOrEditBookmarkViewController.swift b/DuckDuckGo/AddOrEditBookmarkViewController.swift index a079277c93..cc01196dab 100644 --- a/DuckDuckGo/AddOrEditBookmarkViewController.swift +++ b/DuckDuckGo/AddOrEditBookmarkViewController.swift @@ -121,6 +121,7 @@ class AddOrEditBookmarkViewController: UIViewController { WidgetCenter.shared.reloadAllTimelines() self.delegate?.finishedEditing(self, entityID: viewModel.bookmark.objectID) dismiss(animated: true, completion: nil) + (UIApplication.shared.delegate as? AppDelegate)?.requestSyncIfEnabled() } @IBSegueAction func onCreateEditor(_ coder: NSCoder, sender: Any?, segueIdentifier: String?) -> AddOrEditBookmarkViewController? { diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index f367cb24e6..fcc325119b 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -31,6 +31,7 @@ import Crashes import Configuration import Networking import DDGSync +import SyncDataProviders // swiftlint:disable file_length // swiftlint:disable type_body_length @@ -58,7 +59,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { private var lastBackgroundDate: Date? private(set) var syncService: DDGSyncing! - private(set) var syncPersistence: SyncDataPersistor! + private(set) var syncDataProviders: SyncDataProviders! // MARK: lifecycle @@ -151,6 +152,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { LegacyBookmarksStoreMigration.migrate(from: legacyStorage, to: context) legacyStorage?.removeStore() + WidgetCenter.shared.reloadAllTimelines() } @@ -166,7 +168,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { fatalError("Could not create AppTP database stack: \(error?.localizedDescription ?? "err")") } } - + Favicons.shared.migrateFavicons(to: Favicons.Constants.maxFaviconSize) { WidgetCenter.shared.reloadAllTimelines() } @@ -181,12 +183,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate { DaxDialogs.shared.primeForUse() } + // MARK: Sync initialisation + + syncDataProviders = SyncDataProviders(bookmarksDatabase: bookmarksDatabase) + syncService = DDGSync(dataProvidersSource: syncDataProviders, log: .syncLog) + let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: Bundle.main) guard let main = storyboard.instantiateInitialViewController(creator: { coder in MainViewController(coder: coder, bookmarksDatabase: self.bookmarksDatabase, - appTrackingProtectionDatabase: self.appTrackingProtectionDatabase) + appTrackingProtectionDatabase: self.appTrackingProtectionDatabase, + syncService: self.syncService) }) else { fatalError("Could not load MainViewController") } @@ -213,10 +221,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { window?.windowScene?.screenshotService?.delegate = self ThemeManager.shared.updateUserInterfaceStyle(window: window) - // MARK: Sync initialisation - syncPersistence = SyncDataPersistor() - syncService = DDGSync(persistence: syncPersistence) - appIsLaunching = true return true } @@ -286,6 +290,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate { WindowsBrowserWaitlist.shared.scheduleBackgroundRefreshTask() } } + + syncService.scheduler.notifyAppLifecycleEvent() + } + + func requestSyncIfEnabled() { + guard let syncService, syncService.authState == .active else { + os_log(.debug, log: OSLog.syncLog, "Sync disabled, not scheduling") + return + } + os_log(.debug, log: OSLog.syncLog, "Requesting sync") + syncService.scheduler.notifyDataChanged() } private func fireAppLaunchPixel() { diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index fd06b730c4..3b42c75074 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -20,6 +20,8 @@ import Foundation import Core import BrowserServicesKit +import DDGSync +import Bookmarks protocol DependencyProvider { var appSettings: AppSettings { get } diff --git a/DuckDuckGo/BookmarksDatabase.swift b/DuckDuckGo/BookmarksDatabase.swift index 692f4d9e68..b09dc6453d 100644 --- a/DuckDuckGo/BookmarksDatabase.swift +++ b/DuckDuckGo/BookmarksDatabase.swift @@ -58,5 +58,4 @@ public class BookmarksDatabase { globalReferenceForDebug = db return db } - } diff --git a/DuckDuckGo/BookmarksViewController.swift b/DuckDuckGo/BookmarksViewController.swift index 3ce41c1619..6056c293fa 100644 --- a/DuckDuckGo/BookmarksViewController.swift +++ b/DuckDuckGo/BookmarksViewController.swift @@ -102,6 +102,9 @@ class BookmarksViewController: UIViewController, UITableViewDelegate { fileprivate var onDidAppearAction: () -> Void = {} + private var localUpdatesCancellable: AnyCancellable? + private var syncUpdatesCancellable: AnyCancellable? + init?(coder: NSCoder, bookmarksDatabase: CoreDataDatabase, bookmarksSearch: BookmarksStringSearch, @@ -112,12 +115,29 @@ class BookmarksViewController: UIViewController, UITableViewDelegate { self.viewModel = BookmarkListViewModel(bookmarksDatabase: bookmarksDatabase, parentID: parentID) self.favicons = favicons super.init(coder: coder) + + bindSyncService() } - + required init?(coder: NSCoder) { fatalError("Not implemented") } + private func bindSyncService() { + localUpdatesCancellable = viewModel.localUpdates + .sink { _ in + (UIApplication.shared.delegate as? AppDelegate)?.requestSyncIfEnabled() + } + + syncUpdatesCancellable = (UIApplication.shared.delegate as? AppDelegate)?.syncDataProviders.bookmarksAdapter.syncDidCompletePublisher + .sink { [weak self] _ in + self?.viewModel.reloadData() + DispatchQueue.main.async { + self?.tableView.reloadData() + } + } + } + override func viewDidLoad() { super.viewDidLoad() @@ -339,7 +359,7 @@ class BookmarksViewController: UIViewController, UITableViewDelegate { let domains = domainsInBookmarkTree(bookmark) let oldCount = viewModel.bookmarks.count - viewModel.deleteBookmark(bookmark) + viewModel.softDeleteBookmark(bookmark) let newCount = viewModel.bookmarks.count // Make sure we are animating only single removal diff --git a/DuckDuckGo/FavoritesViewController.swift b/DuckDuckGo/FavoritesViewController.swift index b2f550dbd4..5e6b28530a 100644 --- a/DuckDuckGo/FavoritesViewController.swift +++ b/DuckDuckGo/FavoritesViewController.swift @@ -51,7 +51,9 @@ class FavoritesViewController: UIViewController { private let bookmarksDatabase: CoreDataDatabase fileprivate var viewModelCancellable: AnyCancellable? - + private var localUpdatesCancellable: AnyCancellable? + private var syncUpdatesCancellable: AnyCancellable? + var hasFavorites: Bool { renderer.viewModel.favorites.count > 0 } @@ -67,7 +69,7 @@ class FavoritesViewController: UIViewController { self.bookmarksDatabase = bookmarksDatabase super.init(coder: coder) } - + required init?(coder: NSCoder) { fatalError("Not implemented") } @@ -104,6 +106,23 @@ class FavoritesViewController: UIViewController { updateHeroImage() applyTheme(ThemeManager.shared.currentTheme) + + bindSyncService() + } + + private func bindSyncService() { + localUpdatesCancellable = renderer.viewModel.localUpdates + .sink { _ in + (UIApplication.shared.delegate as? AppDelegate)?.requestSyncIfEnabled() + } + + syncUpdatesCancellable = (UIApplication.shared.delegate as? AppDelegate)?.syncDataProviders.bookmarksAdapter.syncDidCompletePublisher + .sink { [weak self] _ in + self?.renderer.viewModel.reloadData() + DispatchQueue.main.async { + self?.collectionView.reloadData() + } + } } override func viewDidLayoutSubviews() { diff --git a/DuckDuckGo/FireproofFaviconUpdater.swift b/DuckDuckGo/FireproofFaviconUpdater.swift index 3e944f0ac6..22c7dc6b2d 100644 --- a/DuckDuckGo/FireproofFaviconUpdater.swift +++ b/DuckDuckGo/FireproofFaviconUpdater.swift @@ -99,11 +99,13 @@ class FireproofFaviconUpdater: NSObject, FaviconUserScriptDelegate { ]) let notFolderPredicate = NSPredicate(format: "%K = NO", #keyPath(BookmarkEntity.isFolder)) + let notDeletedPredicate = NSPredicate(format: "%K = NO", #keyPath(BookmarkEntity.isPendingDeletion)) let request = BookmarkEntity.fetchRequest() request.fetchLimit = 1 request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ notFolderPredicate, + notDeletedPredicate, domainPredicate ]) let result = (try? context.count(for: request)) ?? 0 > 0 diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index e63955fc4c..ff3dd889f7 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -19,8 +19,10 @@ import UIKit import WebKit +import Combine import Common import Core +import DDGSync import Lottie import Kingfisher import BrowserServicesKit @@ -99,10 +101,15 @@ class MainViewController: UIViewController { fileprivate lazy var appSettings: AppSettings = AppUserDefaults() private var launchTabObserver: LaunchTabNotification.Observer? - private let bookmarksDatabase: CoreDataDatabase private let appTrackingProtectionDatabase: CoreDataDatabase + private let bookmarksDatabase: CoreDataDatabase + private let bookmarksDatabaseCleaner: BookmarkDatabaseCleaner private let favoritesViewModel: FavoritesListInteracting - + private let syncService: DDGSyncing + private var syncStateCancellable: AnyCancellable? + private var localUpdatesCancellable: AnyCancellable? + private var syncUpdatesCancellable: AnyCancellable? + lazy var menuBookmarksViewModel: MenuBookmarksInteracting = MenuBookmarksViewModel(bookmarksDatabase: bookmarksDatabase) weak var tabSwitcherController: TabSwitcherViewController? @@ -130,17 +137,25 @@ class MainViewController: UIViewController { // Skip SERP flow (focusing on autocomplete logic) and prepare for new navigation when selecting search bar private var skipSERPFlow = true - + required init?(coder: NSCoder, bookmarksDatabase: CoreDataDatabase, - appTrackingProtectionDatabase: CoreDataDatabase) { - self.bookmarksDatabase = bookmarksDatabase + appTrackingProtectionDatabase: CoreDataDatabase, + syncService: DDGSyncing) { self.appTrackingProtectionDatabase = appTrackingProtectionDatabase + self.bookmarksDatabase = bookmarksDatabase + self.bookmarksDatabaseCleaner = BookmarkDatabaseCleaner( + bookmarkDatabase: bookmarksDatabase, + errorEvents: BookmarksCleanupErrorHandling(), + log: .generalLog + ) + self.syncService = syncService self.favoritesViewModel = FavoritesListViewModel(bookmarksDatabase: bookmarksDatabase) self.bookmarksCachingSearch = BookmarksCachingSearch(bookmarksStore: CoreDataBookmarksSearchStore(bookmarksStore: bookmarksDatabase)) super.init(coder: coder) + bindSyncService() } - + required init?(coder: NSCoder) { fatalError("Use init?(code:") } @@ -307,7 +322,35 @@ class MainViewController: UIViewController { gestureBookmarksButton.delegate = self gestureBookmarksButton.image = UIImage(named: "Bookmarks") } - + + private func bindSyncService() { + syncStateCancellable = syncService.authStatePublisher + .prepend(syncService.authState) + .map { $0 == .inactive } + .removeDuplicates() + .sink { [weak self] isSyncDisabled in + self?.bookmarksDatabaseCleaner.cleanUpDatabaseNow() + if isSyncDisabled { + self?.bookmarksDatabaseCleaner.scheduleRegularCleaning() + } else { + self?.bookmarksDatabaseCleaner.cancelCleaningSchedule() + } + } + + localUpdatesCancellable = favoritesViewModel.localUpdates + .sink { _ in + (UIApplication.shared.delegate as? AppDelegate)?.requestSyncIfEnabled() + } + + syncUpdatesCancellable = (UIApplication.shared.delegate as? AppDelegate)?.syncDataProviders.bookmarksAdapter.syncDidCompletePublisher + .sink { [weak self] _ in + self?.favoritesViewModel.reloadData() + DispatchQueue.main.async { + self?.homeController?.collectionView.reloadData() + } + } + } + @objc func quickSaveBookmarkLongPress(gesture: UILongPressGestureRecognizer) { if gesture.state == .began { quickSaveBookmark() @@ -1790,8 +1833,12 @@ extension MainViewController: AutoClearWorker { } AutoconsentManagement.shared.clearCache() - DaxDialogs.shared.clearHeldURLData() + + let syncService = (UIApplication.shared.delegate as? AppDelegate)!.syncService + if syncService?.authState == .inactive { + bookmarksDatabaseCleaner.cleanUpDatabaseNow() + } } func stopAllOngoingDownloads() { diff --git a/DuckDuckGo/RemoteMessaging.swift b/DuckDuckGo/RemoteMessaging.swift index ab5ea8f6f5..b45f60908d 100644 --- a/DuckDuckGo/RemoteMessaging.swift +++ b/DuckDuckGo/RemoteMessaging.swift @@ -103,15 +103,17 @@ struct RemoteMessaging { let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) context.performAndWait { let bookmarksCountRequest = BookmarkEntity.fetchRequest() - bookmarksCountRequest.predicate = NSPredicate(format: "%K == false AND %K == false", - #keyPath(BookmarkEntity.isFavorite), - #keyPath(BookmarkEntity.isFolder)) + bookmarksCountRequest.predicate = NSPredicate(format: "%K == nil AND %K == false AND %K == false", + #keyPath(BookmarkEntity.favoriteFolder), + #keyPath(BookmarkEntity.isFolder), + #keyPath(BookmarkEntity.isPendingDeletion)) bookmarksCount = (try? context.count(for: bookmarksCountRequest)) ?? 0 let favoritesCountRequest = BookmarkEntity.fetchRequest() - bookmarksCountRequest.predicate = NSPredicate(format: "%K == true AND %K == false", - #keyPath(BookmarkEntity.isFavorite), - #keyPath(BookmarkEntity.isFolder)) + bookmarksCountRequest.predicate = NSPredicate(format: "%K != nil AND %K == false AND %K == false", + #keyPath(BookmarkEntity.favoriteFolder), + #keyPath(BookmarkEntity.isFolder), + #keyPath(BookmarkEntity.isPendingDeletion)) favoritesCount = (try? context.count(for: favoritesCountRequest)) ?? 0 } diff --git a/DuckDuckGo/SyncDataPersistor.swift b/DuckDuckGo/SyncDataPersistor.swift deleted file mode 100644 index 5465adfb3e..0000000000 --- a/DuckDuckGo/SyncDataPersistor.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// SyncDataPersistor.swift -// DuckDuckGo -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import DDGSync - -final class SyncDataPersistor: LocalDataPersisting { - private(set) var bookmarksLastModified: String? - - func updateBookmarksLastModified(_ lastModified: String?) { - bookmarksLastModified = lastModified - } - - func persistEvents(_ events: [SyncEvent]) async throws { - } -} diff --git a/DuckDuckGo/SyncSettingsViewController.swift b/DuckDuckGo/SyncSettingsViewController.swift index 34902a9a2d..7403899072 100644 --- a/DuckDuckGo/SyncSettingsViewController.swift +++ b/DuckDuckGo/SyncSettingsViewController.swift @@ -53,16 +53,16 @@ class SyncSettingsViewController: UIHostingController { self.init(rootView: SyncSettingsView(model: SyncSettingsViewModel())) // For some reason, on iOS 14, the viewDidLoad wasn't getting called so do some setup here - if syncService.isAuthenticated { + if syncService.authState == .active { rootView.model.syncEnabled(recoveryCode: recoveryCode) refreshDevices() } - syncService.isAuthenticatedPublisher + syncService.authStatePublisher .removeDuplicates() .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.rootView.model.isSyncEnabled = self!.syncService.isAuthenticated + .sink { [weak self] authState in + self?.rootView.model.isSyncEnabled = authState != .inactive } .store(in: &cancellables) @@ -85,7 +85,7 @@ class SyncSettingsViewController: UIHostingController { } func refreshDevices(clearDevices: Bool = true) { - guard syncService.isAuthenticated else { return } + guard syncService.authState != .inactive else { return } Task { @MainActor in if clearDevices { diff --git a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift index 06b8ccb405..f385c7234c 100644 --- a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift +++ b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift @@ -217,6 +217,7 @@ extension TabViewController { Pixel.fire(pixel: .browsingMenuAddToBookmarks) bookmarksInterface.createBookmark(title: link.title ?? "", url: link.url) favicons.loadFavicon(forDomain: link.url.host, intoCache: .fireproof, fromCache: .tabs) + (UIApplication.shared.delegate as? AppDelegate)?.requestSyncIfEnabled() ActionMessageView.present(message: UserText.webSaveBookmarkDone, actionTitle: UserText.actionGenericEdit, onAction: { diff --git a/DuckDuckGo/TabViewControllerLongPressBookmarkExtension.swift b/DuckDuckGo/TabViewControllerLongPressBookmarkExtension.swift index 1ea8b033c7..510e01882d 100644 --- a/DuckDuckGo/TabViewControllerLongPressBookmarkExtension.swift +++ b/DuckDuckGo/TabViewControllerLongPressBookmarkExtension.swift @@ -32,12 +32,15 @@ extension TabViewController { if favorite && nil == viewModel.favorite(for: link.url) { viewModel.createOrToggleFavorite(title: link.displayTitle, url: link.url) WidgetCenter.shared.reloadAllTimelines() - + (UIApplication.shared.delegate as? AppDelegate)?.requestSyncIfEnabled() + DispatchQueue.main.async { ActionMessageView.present(message: UserText.webSaveFavoriteDone) } } else if nil == viewModel.bookmark(for: link.url) { viewModel.createBookmark(title: link.displayTitle, url: link.url) + (UIApplication.shared.delegate as? AppDelegate)?.requestSyncIfEnabled() + DispatchQueue.main.async { ActionMessageView.present(message: UserText.webSaveBookmarkDone) } diff --git a/DuckDuckGoTests/BookmarkEditorViewModelTests.swift b/DuckDuckGoTests/BookmarkEditorViewModelTests.swift index 8b38ef680d..a293c8fc3a 100644 --- a/DuckDuckGoTests/BookmarkEditorViewModelTests.swift +++ b/DuckDuckGoTests/BookmarkEditorViewModelTests.swift @@ -141,9 +141,11 @@ class BookmarkEditorViewModelTests: XCTestCase { let folders = model.locations let fetchFolders = BookmarkEntity.fetchRequest() - fetchFolders.predicate = NSPredicate(format: "%K == true AND %K != %@", #keyPath(BookmarkEntity.isFolder), - #keyPath(BookmarkEntity.uuid), - BookmarkEntity.Constants.favoritesFolderID) + fetchFolders.predicate = NSPredicate(format: "%K == true AND %K != %@ AND %K == false", + #keyPath(BookmarkEntity.isFolder), + #keyPath(BookmarkEntity.uuid), + BookmarkEntity.Constants.favoritesFolderID, + #keyPath(BookmarkEntity.isPendingDeletion)) let allFolders = (try? context.fetch(fetchFolders)) ?? [] XCTAssertEqual(folders.count, allFolders.count) diff --git a/DuckDuckGoTests/BookmarkEntityTests.swift b/DuckDuckGoTests/BookmarkEntityTests.swift index 56722d4755..3a9cf3aad9 100644 --- a/DuckDuckGoTests/BookmarkEntityTests.swift +++ b/DuckDuckGoTests/BookmarkEntityTests.swift @@ -84,22 +84,6 @@ class BookmarkEntityTests: XCTestCase { try context.save() } - func testWhenBookmarkIsMissingURLThenValidationFails() { - let bookmark = BookmarkEntity.makeBookmark(title: "t", - url: "u", - parent: root, - context: context) - - bookmark.url = nil - - do { - try context.save() - XCTFail("Save should fail") - } catch { - - } - } - func testWhenFavoriteIsUnderWrongFavoriteRootThenValidationFails() { let favorite = BookmarkEntity.makeBookmark(title: "t", url: "u", @@ -116,22 +100,6 @@ class BookmarkEntityTests: XCTestCase { } } - func testWhenFavoriteIsMissingFavoriteRootThenValidationFails() { - let favorite = BookmarkEntity.makeBookmark(title: "t", - url: "u", - parent: root, - context: context) - - favorite.setValue(true, forKey: #keyPath(BookmarkEntity.isFavorite)) - - do { - try context.save() - XCTFail("Save should fail") - } catch { - - } - } - func testWhenFolderHasURLThenValidationFails() { let folder = BookmarkEntity.makeFolder(title: "f", parent: root, context: context) diff --git a/DuckDuckGoTests/BookmarkListViewModelTests.swift b/DuckDuckGoTests/BookmarkListViewModelTests.swift index 8be3f8378b..a2ff0e6108 100644 --- a/DuckDuckGoTests/BookmarkListViewModelTests.swift +++ b/DuckDuckGoTests/BookmarkListViewModelTests.swift @@ -96,7 +96,7 @@ class BookmarkListViewModelTests: XCTestCase { let bookmark = result[0] XCTAssertFalse(bookmark.isFolder) - viewModel.deleteBookmark(bookmark) + viewModel.softDeleteBookmark(bookmark) let newViewModel = BookmarkListViewModel(bookmarksDatabase: db, parentID: nil) @@ -122,7 +122,7 @@ class BookmarkListViewModelTests: XCTestCase { let totalCount = viewModel.totalBookmarksCount let expectedCountAfterRemoval = totalCount - folder.childrenArray.filter { !$0.isFolder }.count - viewModel.deleteBookmark(folder) + viewModel.softDeleteBookmark(folder) let newViewModel = BookmarkListViewModel(bookmarksDatabase: db, parentID: nil) @@ -181,7 +181,7 @@ class BookmarkListViewModelTests: XCTestCase { expectation.fulfill() }) { let startState = viewModel.bookmarks - viewModel.deleteBookmark(startState[0]) + viewModel.softDeleteBookmark(startState[0]) waitForExpectations(timeout: 1) diff --git a/DuckDuckGoTests/BookmarksImporterTests.swift b/DuckDuckGoTests/BookmarksImporterTests.swift index 18cc72ca53..001a488c3d 100644 --- a/DuckDuckGoTests/BookmarksImporterTests.swift +++ b/DuckDuckGoTests/BookmarksImporterTests.swift @@ -141,7 +141,9 @@ class BookmarksImporterTests: XCTestCase { try await importer.saveBookmarks(initialBookmarks) let countRequest = BookmarkEntity.fetchRequest() - countRequest.predicate = NSPredicate(format: "%K == false", #keyPath(BookmarkEntity.isFolder)) + countRequest.predicate = NSPredicate(format: "%K == false AND %K == false", + #keyPath(BookmarkEntity.isFolder), + #keyPath(BookmarkEntity.isPendingDeletion)) let count = try storage.makeContext(concurrencyType: .mainQueueConcurrencyType).count(for: countRequest) XCTAssertEqual(count, 2) diff --git a/DuckDuckGoTests/BookmarksIndexesTests.swift b/DuckDuckGoTests/BookmarksIndexesTests.swift new file mode 100644 index 0000000000..048858a926 --- /dev/null +++ b/DuckDuckGoTests/BookmarksIndexesTests.swift @@ -0,0 +1,112 @@ +// +// BookmarksIndexesTests.swift +// DuckDuckGo +// +// Copyright © 2017 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Persistence +import Bookmarks + +final class BookmarksIndexesTests: XCTestCase { + var bookmarksDatabase: CoreDataDatabase! + var location: URL! + + override func setUp() { + super.setUp() + + location = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + + let bundle = Bookmarks.bundle + guard let model = CoreDataDatabase.loadModel(from: bundle, named: "BookmarksModel") else { + XCTFail("Failed to load model") + return + } + bookmarksDatabase = CoreDataDatabase(name: "BookmarksIndexesTests", containerLocation: location, model: model) + bookmarksDatabase.loadStore() + } + + override func tearDown() { + super.tearDown() + + try? bookmarksDatabase.tearDown(deleteStores: true) + bookmarksDatabase = nil + try? FileManager.default.removeItem(at: location) + } + + private func populateDatabase(_ database: CoreDataDatabase, _ numberOfItems: Int) { + let context = database.makeContext(concurrencyType: .privateQueueConcurrencyType) + + context.performAndWait { + BookmarkUtils.prepareFoldersStructure(in: context) + + do { + try context.save() + } catch { + XCTFail(error.localizedDescription) + } + + guard let rootFolder = BookmarkUtils.fetchRootFolder(context) else { + XCTFail("Failed to find root folder") + return + } + + (0..