Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean removed contacts from DB #955

Merged
merged 3 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions MailCore/Cache/ContactManager/ContactManager+DB.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import RealmSwift
public extension ContactManager {
func getRealm() -> Realm {
do {
return try Realm(configuration: realmConfiguration)
let realm = try Realm(configuration: realmConfiguration)
realm.refresh()
return realm
} catch {
// We can't recover from this error but at least we report it correctly on Sentry
Logging.reportRealmOpeningError(error, realmConfiguration: realmConfiguration)
Expand Down Expand Up @@ -52,7 +54,7 @@ public extension ContactManager {
// Iterate a given number of times to emulate a `LIMIT` statement.
var iterator = lazyResults.makeIterator()
var results = [MergedContact]()
for _ in 0..<fetchLimit {
for _ in 0 ..< fetchLimit {
guard let next = iterator.next() else {
break
}
Expand Down Expand Up @@ -85,8 +87,7 @@ public extension ContactManager {

guard let newContact = contacts.first(where: { $0.id == String(contactId) }) else { throw MailError.contactNotFound }

let email = recipient.email
guard let mergedContact = MergedContact(email: email, local: nil, remote: newContact) else { return }
let mergedContact = MergedContact(email: recipient.email, local: nil, remote: newContact)

let realm = getRealm()
try? realm.safeWrite {
Expand Down
199 changes: 127 additions & 72 deletions MailCore/Cache/ContactManager/ContactManager+Merge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,64 @@ extension CNContact {
}

extension ContactManager {
func merge(localInto remote: [InfomaniakContact]) async {
// Refresh Realm, and trigger redraws, only if app is in foreground.
defer {
Task { @MainActor in
let state = UIApplication.shared.applicationState
if state == .active {
DDLogInfo("Done merging remote and local contacts, refreshing DB…")
getRealm().refresh()
}
// MARK: - Public

/// Making sure only one update task is running or return
///
/// This will merge Infomaniak contacts with local iPhone ones in a coherent DB.
/// Removed contacts from both datasets will be cleaned also.
public func uniqueUpdateContactDBTask(_ apiFetcher: MailApiFetcher) async {
// We do not run an update of contacts in extension mode as we are too resource constrained
guard !Bundle.main.isExtension else {
DDLogInfo("Skip updating contacts, we are in extension mode")
return
}

// Unique task running
guard currentMergeRequest == nil else {
DDLogInfo("UpdateContactDB is running, exiting …")
return
}

DDLogInfo("Will start updating contacts in DB")

// Track background refresh of contacts, and cancel task is system asks to.
let backgroundTaskTracker = await ApplicationBackgroundTaskTracker(identifier: #function + UUID().uuidString) {
self.currentMergeRequest?.cancel()
}

let updateTask = Task {
// Fetch remote contacts
let remoteContacts: [InfomaniakContact]
if let remote = try? await apiFetcher.contacts() {
remoteContacts = remote
} else {
remoteContacts = []
}

// Update DB with them
await updateContactDB(remoteContacts)
}
currentMergeRequest = updateTask

// Await for completion
await updateTask.finish()

// Making sure to terminate BG work tracker
await backgroundTaskTracker.end()

// cleanup
currentMergeRequest = nil
}

/// This reprensets the complete process of maintaining a coherent DB of contacts in realm
///
/// This will merge InfomaniakContact with local iPhone ones in a coherent DB.
/// Removed contacts from both datasets will be cleaned also
/// - Parameter remote: a list of remote infomaniak contacts
internal func updateContactDB(_ remote: [InfomaniakContact]) async {
defer {
DDLogInfo("Done merging remote and local contacts in DB…")
}

// index remote account per email
Expand All @@ -53,28 +101,64 @@ extension ContactManager {
}
}

// Clean contacts no longer present is remote or local iPhone contacts
removeDeletedContactsFromLocal(andRemote: remoteContactsByEmail)

// Insert all the local contacts, while merging them with the remote version
let remainingContacts = await insertLocalContactsInDBMerging(remote: remoteContactsByEmail)

// Insert remaining remote contacts in db
insertContactsInDB(remainingContacts)
}

// Insert Contacts indexed by email in base without check
private func insertContactsInDB(_ input: [String: InfomaniakContact]) {
for (email, contact) in input {
// MARK: - Private

/// Clean contacts no longer present is remote ik or local iPhone contacts
private func removeDeletedContactsFromLocal(andRemote remote: [String: InfomaniakContact]) {
var toDelete = [String]()

// enumerate realm contacts
let lazyContacts = getRealm()
.objects(MergedContact.self)

var contactIterator = lazyContacts.makeIterator()
while let mergedContact = contactIterator.next() {
guard !Task.isCancelled else {
break
return
}

// insert reminder of remote contacts
guard let mergedContact = MergedContact(email: email, local: nil, remote: contact) else {
return
let email = mergedContact.email

let remote = remote[email]
let local: CNContact?
if let localContactId = mergedContact.localIdentifier {
local = try? localContactsHelper.getContact(with: localContactId)
} else {
local = nil
}

let realm = getRealm()
try? realm.safeWrite {
realm.add(mergedContact, update: .modified)
// If contact not in iPhone nor in remote infomaniak, then should be deleted
if remote == nil && local == nil {
toDelete.append(email)
}
}

guard !Task.isCancelled else {
return
}

// remove old contacts
guard !toDelete.isEmpty else {
return
}

let cleanupRealm = getRealm()
try? cleanupRealm.safeWrite {
for emailToDelete in toDelete {
guard let objectToDelete = cleanupRealm.object(ofType: MergedContact.self, forPrimaryKey: emailToDelete) else {
continue
}
cleanupRealm.delete(objectToDelete)
}
}
}
Expand All @@ -86,29 +170,28 @@ extension ContactManager {
var output = input

await localContactsHelper.enumerateContacts { localContact, stop in
if Task.isCancelled {
guard !Task.isCancelled else {
stop.pointee = true
return
}

// For each email of a specific contact
for cnEmail in localContact.emailAddresses {
let email = String(cnEmail.value)

// lookup matching remote contact for current email
let remoteContact = input[email]
// Realm to use for this contact. Do not call it outside this block, or it may crash.
let realm = self.getRealm()
try? realm.safeWrite {
// For each email of a specific contact
for cnEmail in localContact.emailAddresses {
let email = String(cnEmail.value)

// lookup matching remote contact for current email
let remoteContact = input[email]

// Create DB object
guard let mergedContact = MergedContact(email: email, local: localContact, remote: remoteContact) else {
return
}
// Create DB object
let mergedContact = MergedContact(email: email, local: localContact, remote: remoteContact)

// Remove email from lookup table
output.removeValue(forKey: email)
// Remove email from lookup table
output.removeValue(forKey: email)

// Store result
let realm = self.getRealm()
try? realm.safeWrite {
// Store result
realm.add(mergedContact, update: .modified)
}
}
Expand All @@ -117,48 +200,20 @@ extension ContactManager {
return output
}

/// Making sure only one update task is running or return
func uniqueMergeLocalTask(_ apiFetcher: MailApiFetcher) async {
// We do not run an update of contacts in extension mode as we are too resource constrained
guard !Bundle.main.isExtension else {
DDLogInfo("Skip updating contacts, we are in extension mode")
return
}

// Unique task running
guard currentMergeRequest == nil else {
DDLogInfo("Merging contacts running exiting …")
// Insert Contacts indexed by email in base without check
private func insertContactsInDB(_ input: [String: InfomaniakContact]) {
guard !Task.isCancelled else {
return
}

DDLogInfo("Will start merging contacts cancelling previous task : \(currentMergeRequest != nil)")

// Track background refresh of contacts, and cancel task is system asks to.
let backgroundTaskTracker = await ApplicationBackgroundTaskTracker(identifier: #function + UUID().uuidString) {
self.currentMergeRequest?.cancel()
}
let realm = getRealm()
try? realm.safeWrite {
for (email, contact) in input {
// insert reminder of remote contacts
let mergedContact = MergedContact(email: email, local: nil, remote: contact)

let updateTask = Task {
// Fetch remote contacts
let remoteContacts: [InfomaniakContact]
if let remote = try? await apiFetcher.contacts() {
remoteContacts = remote
} else {
remoteContacts = []
realm.add(mergedContact, update: .modified)
}

// Merge them
await merge(localInto: remoteContacts)
}
currentMergeRequest = updateTask

// Await for completion
await updateTask.finish()

// Making sure to terminate BG work tracker
await backgroundTaskTracker.end()

// cleanup
currentMergeRequest = nil
}
}
4 changes: 2 additions & 2 deletions MailCore/Cache/ContactManager/ContactManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,10 @@ public final class ContactManager: ObservableObject {
await backgroundTaskTracker.end()

// Process Contacts
await uniqueMergeLocalTask(apiFetcher)
await uniqueUpdateContactDBTask(apiFetcher)
} catch {
// Process Contacts anyway
await uniqueMergeLocalTask(apiFetcher)
await uniqueUpdateContactDBTask(apiFetcher)

throw error
}
Expand Down
2 changes: 1 addition & 1 deletion MailCore/Models/MergedContact.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public final class MergedContact: Object, Identifiable {
override public init() { /* Realm needs an empty constructor */ }

/// Init with what you have, it will generate the most usable contact possible
public init?(email: String, local: CNContact?, remote: InfomaniakContact?) {
public init(email: String, local: CNContact?, remote: InfomaniakContact?) {
super.init()

self.email = email
Expand Down
Loading