Эх сурвалжийг харах

Merge pull request #1272 from deltachat/DcAccounts

DcAccounts
cyBerta 4 жил өмнө
parent
commit
0da01ba411

+ 105 - 40
DcCore/DcCore/DC/Wrapper.swift

@@ -2,6 +2,95 @@ import Foundation
 import UIKit
 import AVFoundation
 
+public class DcAccounts {
+
+    /// The application group identifier defines a group of apps or extensions that have access to a shared container.
+    /// The ID is created in the apple developer portal and can be changed there.
+    let applicationGroupIdentifier = "group.chat.delta.ios"
+    var accountsPointer: OpaquePointer?
+
+    public init() {
+    }
+
+    deinit {
+        if accountsPointer == nil { return }
+        dc_accounts_unref(accountsPointer)
+        accountsPointer = nil
+    }
+
+    public func migrate(dbLocation: String) -> Int {
+        return Int(dc_accounts_migrate_account(accountsPointer, dbLocation))
+    }
+
+    public func add() -> Int {
+        return Int(dc_accounts_add_account(accountsPointer))
+    }
+
+    public func get(id: Int) -> DcContext {
+        let contextPointer = dc_accounts_get_account(accountsPointer, UInt32(id))
+        return DcContext(contextPointer: contextPointer)
+    }
+
+    public func getAll() -> [Int] {
+        let cAccounts = dc_accounts_get_all(accountsPointer)
+        return DcUtils.copyAndFreeArray(inputArray: cAccounts)
+    }
+
+    public func getSelected() -> DcContext {
+        let cPtr = dc_accounts_get_selected_account(accountsPointer)
+        return DcContext(contextPointer: cPtr)
+    }
+
+    // call maybeNetwork() from a worker thread.
+    public func maybeNetwork() {
+        dc_accounts_maybe_network(accountsPointer)
+    }
+
+    public func maybeStartIo() {
+        if getSelected().isConfigured() {
+            dc_accounts_start_io(accountsPointer)
+        }
+    }
+
+    public func stopIo() {
+        dc_accounts_stop_io(accountsPointer)
+    }
+
+    public func select(id: Int) -> Bool {
+        return dc_accounts_select_account(accountsPointer, UInt32(id)) == 1
+    }
+
+    public func remove(id: Int) -> Bool {
+        return dc_accounts_remove_account(accountsPointer, UInt32(id)) == 1
+    }
+
+    public func importAccount(filePath: String) -> Int {
+        return Int(dc_accounts_import_account(accountsPointer, filePath))
+    }
+
+    public func getEventEmitter() -> DcAccountsEventEmitter {
+        let eventEmitterPointer = dc_accounts_get_event_emitter(accountsPointer)
+        return DcAccountsEventEmitter(eventEmitterPointer: eventEmitterPointer)
+    }
+
+    public func openDatabase() {
+        var version = ""
+        if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
+            version += " " + appVersion
+        }
+
+        if var sharedDbLocation = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: applicationGroupIdentifier) {
+            sharedDbLocation.appendPathComponent("accounts", isDirectory: true)
+            accountsPointer = dc_accounts_new("iOS\(version)", sharedDbLocation.path)
+        }
+    }
+
+    public func closeDatabase() {
+        dc_accounts_unref(accountsPointer)
+        accountsPointer = nil
+    }
+}
+
 public class DcContext {
 
     public var logger: Logger?
@@ -12,6 +101,10 @@ public class DcContext {
 
     public init() {
     }
+
+    public init(contextPointer: OpaquePointer?) {
+        self.contextPointer = contextPointer
+    }
     
     deinit {
         if contextPointer == nil { return } // avoid a warning about a "careless call"
@@ -19,6 +112,10 @@ public class DcContext {
         contextPointer = nil
     }
 
+    public var id: Int {
+        return Int(dc_get_id(contextPointer))
+    }
+
     // viewType: one of DC_MSG_*
     public func newMessage(viewType: Int32) -> DcMsg {
         let messagePointer = dc_msg_new(contextPointer, viewType)
@@ -219,38 +316,6 @@ public class DcContext {
         return "ErrGetContactEncrInfo"
     }
 
-    public func interruptIdle() {
-    }
-
-    public func getEventEmitter() -> DcEventEmitter {
-        let eventEmitterPointer = dc_get_event_emitter(contextPointer)
-        return DcEventEmitter(eventEmitterPointer: eventEmitterPointer)
-    }
-
-    public func openDatabase(dbFile: String) {
-        var version = ""
-        if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
-            version += " " + appVersion
-        }
-
-        contextPointer = dc_context_new("iOS" + version, dbFile, nil)
-    }
-
-    public func closeDatabase() {
-        dc_context_unref(contextPointer)
-        contextPointer = nil
-    }
-
-    public func maybeStartIo() {
-        if isConfigured() {
-            dc_start_io(contextPointer)
-        }
-    }
-
-    public func stopIo() {
-        dc_stop_io(contextPointer)
-    }
-
     public func setStockTranslation(id: Int32, localizationKey: String) {
         dc_set_stock_translation(contextPointer, UInt32(id), String.localized(localizationKey))
     }
@@ -462,11 +527,6 @@ public class DcContext {
         return messageIds
     }
 
-    // call dc_maybe_network() from a worker thread.
-    public func maybeNetwork() {
-        dc_maybe_network(contextPointer)
-    }
-
     // also, there is no much worth in adding a separate function or so
     // for each config option - esp. if they are just forwarded to the core
     // and set/get only at one line of code each.
@@ -596,7 +656,8 @@ public class DcContext {
     }
 }
 
-public class DcEventEmitter {
+
+public class DcAccountsEventEmitter {
     private var eventEmitterPointer: OpaquePointer?
 
     // takes ownership of specified pointer
@@ -605,12 +666,12 @@ public class DcEventEmitter {
     }
 
     public func getNextEvent() -> DcEvent? {
-        guard let eventPointer = dc_get_next_event(eventEmitterPointer) else { return nil }
+        guard let eventPointer = dc_accounts_get_next_event(eventEmitterPointer) else { return nil }
         return DcEvent(eventPointer: eventPointer)
     }
 
     deinit {
-        dc_event_emitter_unref(eventEmitterPointer)
+        dc_accounts_event_emitter_unref(eventEmitterPointer)
     }
 }
 
@@ -626,6 +687,10 @@ public class DcEvent {
         dc_event_unref(eventPointer)
     }
 
+    public var accountId: Int {
+        return Int(dc_event_get_account_id(eventPointer))
+    }
+
     public var id: Int32 {
         return Int32(dc_event_get_id(eventPointer))
     }

+ 30 - 5
DcCore/DcCore/DC/events.swift

@@ -13,16 +13,17 @@ public let dcEphemeralTimerModified =  Notification.Name(rawValue: "dcEphemeralT
 public let dcMsgsNoticed = Notification.Name(rawValue: "dcMsgsNoticed")
 
 public class DcEventHandler {
-    let dcContext: DcContext
+    let dcAccounts: DcAccounts
 
-    public init(dcContext: DcContext) {
-        self.dcContext = dcContext
+    public init(dcAccounts: DcAccounts) {
+        self.dcAccounts = dcAccounts
     }
 
     public func handleEvent(event: DcEvent) {
         let id = event.id
         let data1 = event.data1Int
         let data2 = event.data2Int
+        let dcContext = dcAccounts.get(id: event.accountId)
 
         if id >= DC_EVENT_ERROR && id <= 499 {
             let s = event.data2String
@@ -68,7 +69,7 @@ public class DcEventHandler {
 
         case DC_EVENT_IMEX_PROGRESS:
             let nc = NotificationCenter.default
-            DispatchQueue.main.async { [weak self] in
+            DispatchQueue.main.async {
                 nc.post(
                     name: dcNotificationImexProgress,
                     object: nil,
@@ -76,7 +77,7 @@ public class DcEventHandler {
                         "progress": Int(data1),
                         "error": Int(data1) == 0,
                         "done": Int(data1) == 1000,
-                        "errorMessage": self?.dcContext.lastErrorString as Any,
+                        "errorMessage": dcContext.lastErrorString as Any,
                     ]
                 )
             }
@@ -85,6 +86,9 @@ public class DcEventHandler {
             dcContext.logger?.warning("network: \(event.data2String)")
 
         case DC_EVENT_MSGS_CHANGED, DC_EVENT_MSG_READ, DC_EVENT_MSG_DELIVERED, DC_EVENT_MSG_FAILED:
+            if dcContext.id != dcAccounts.getSelected().id {
+                return
+            }
             dcContext.logger?.info("change: \(id)")
 
             let nc = NotificationCenter.default
@@ -102,6 +106,9 @@ public class DcEventHandler {
             }
 
         case DC_EVENT_MSGS_NOTICED:
+            if dcContext.id != dcAccounts.getSelected().id {
+                return
+            }
             let nc = NotificationCenter.default
             DispatchQueue.main.async {
                 nc.post(
@@ -114,6 +121,9 @@ public class DcEventHandler {
             }
 
         case DC_EVENT_CHAT_MODIFIED:
+            if dcContext.id != dcAccounts.getSelected().id {
+                return
+            }
             dcContext.logger?.info("chat modified: \(id)")
             let nc = NotificationCenter.default
             DispatchQueue.main.async {
@@ -126,6 +136,9 @@ public class DcEventHandler {
                 )
             }
         case DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED:
+            if dcContext.id != dcAccounts.getSelected().id {
+                return
+            }
             dcContext.logger?.info("chat ephemeral timer modified: \(id)")
             let nc = NotificationCenter.default
             DispatchQueue.main.async {
@@ -137,6 +150,9 @@ public class DcEventHandler {
             }
 
         case DC_EVENT_INCOMING_MSG:
+            if dcContext.id != dcAccounts.getSelected().id {
+                return
+            }
             let nc = NotificationCenter.default
             let userInfo = [
                 "message_id": Int(data2),
@@ -157,6 +173,9 @@ public class DcEventHandler {
             dcContext.logger?.info("message delivered: \(data1)-\(data2)")
 
         case DC_EVENT_SECUREJOIN_INVITER_PROGRESS:
+            if dcContext.id != dcAccounts.getSelected().id {
+                return
+            }
             dcContext.logger?.info("securejoin inviter progress \(data1)")
 
             let nc = NotificationCenter.default
@@ -173,6 +192,9 @@ public class DcEventHandler {
             }
 
         case DC_EVENT_SECUREJOIN_JOINER_PROGRESS:
+            if dcContext.id != dcAccounts.getSelected().id {
+                return
+            }
             dcContext.logger?.info("securejoin joiner progress \(data1)")
             let nc = NotificationCenter.default
             DispatchQueue.main.async {
@@ -188,6 +210,9 @@ public class DcEventHandler {
                 )
             }
         case DC_EVENT_CONTACTS_CHANGED:
+            if dcContext.id != dcAccounts.getSelected().id {
+                return
+            }
             dcContext.logger?.info("contact changed: \(data1)")
             let nc = NotificationCenter.default
             DispatchQueue.main.async {

+ 5 - 85
DcCore/DcCore/Helper/DatabaseHelper.swift

@@ -17,101 +17,21 @@ public class DatabaseHelper {
         return localDocumentsDir.appendingPathComponent("messenger.db").path
     }
 
-    var sharedDbBlobsDir: String {
-        guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: DatabaseHelper.applicationGroupIdentifier) else {
-            return ""
-        }
-        return fileContainer.appendingPathComponent("messenger.db-blobs").path
-    }
-
-    var localDbBlobsDir: String {
-        return localDocumentsDir.appendingPathComponent("messenger.db-blobs").path
-    }
-
     var localDocumentsDir: URL {
         let paths = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true)
         return URL(fileURLWithPath: paths[0], isDirectory: true)
     }
 
-    public var currentDatabaseLocation: String {
+    public var unmanagedDatabaseLocation: String? {
         let filemanager = FileManager.default
         if filemanager.fileExists(atPath: localDbFile) {
             return localDbFile
+        } else if filemanager.fileExists(atPath: sharedDbFile) {
+            return sharedDbFile
         }
-        return sharedDbFile
-    }
-
-    var currentBlobsDirLocation: String {
-        let filemanager = FileManager.default
-        if filemanager.fileExists(atPath: localDbBlobsDir) {
-            return localDbBlobsDir
-        }
-        return sharedDbBlobsDir
-    }
-
-    var dcContext: DcContext
-
-    public init(dcContext: DcContext) {
-        self.dcContext = dcContext
-    }
-
-    func clearDbBlobsDir(at path: String) {
-        let fileManager = FileManager.default
-        do {
-            if fileManager.fileExists(atPath: path) {
-                let filePaths =  try fileManager.contentsOfDirectory(atPath: path)
-                for filePath in filePaths {
-                    let completePath = URL(fileURLWithPath: path).appendingPathComponent(filePath)
-                    try fileManager.removeItem(atPath: completePath.path)
-                }
-                try fileManager.removeItem(atPath: path)
-            }
-        } catch {
-            dcContext.logger?.error("Could not clean shared blobs dir, it might be it didn't exist")
-        }
-    }
-
-    func clearDb(at path: String) {
-        let filemanager = FileManager.default
-        if filemanager.fileExists(atPath: path) {
-            do {
-                try filemanager.removeItem(atPath: path)
-            } catch {
-                dcContext.logger?.error("Failed to delete db: \(error)")
-            }
-        }
+        return nil
     }
 
-    public func clearAccountData() {
-        clearDb(at: currentDatabaseLocation)
-        clearDbBlobsDir(at: currentBlobsDirLocation)
-    }
-
-    func moveBlobsFolder() {
-        let filemanager = FileManager.default
-        if filemanager.fileExists(atPath: localDbBlobsDir) {
-            do {
-                clearDbBlobsDir(at: sharedDbBlobsDir)
-                try filemanager.moveItem(at: URL(fileURLWithPath: localDbBlobsDir), to: URL(fileURLWithPath: sharedDbBlobsDir))
-            } catch let error {
-                dcContext.logger?.error("Could not move db blobs directory to shared space: \(error.localizedDescription)")
-            }
-        }
-    }
-
-    public func updateDatabaseLocation() -> String? {
-      let filemanager = FileManager.default
-      if filemanager.fileExists(atPath: localDbFile) {
-          do {
-              clearDb(at: sharedDbFile)
-              try filemanager.moveItem(at: URL(fileURLWithPath: localDbFile), to: URL(fileURLWithPath: sharedDbFile))
-              moveBlobsFolder()
-          } catch let error {
-              dcContext.logger?.error("Could not update DB location. Share extension will probably not work. \n\(error.localizedDescription)")
-              return localDbFile
-          }
-      }
-      return sharedDbFile
-    }
+    public init() {}
 
 }

+ 23 - 27
DcShare/Controller/ShareViewController.swift

@@ -31,12 +31,12 @@ class ShareViewController: SLComposeServiceViewController {
         }
     }
 
-    lazy var dbHelper: DatabaseHelper = {
-       return DatabaseHelper(dcContext: dcContext)
+    let logger = SimpleLogger()
+    let dcAccounts: DcAccounts = DcAccounts()
+    lazy var dcContext: DcContext = {
+        return dcAccounts.getSelected()
     }()
 
-    let logger = SimpleLogger()
-    let dcContext: DcContext = DcContext()
     var selectedChatId: Int?
     var selectedChat: DcChat?
     var shareAttachment: ShareAttachment?
@@ -77,27 +77,25 @@ class ShareViewController: SLComposeServiceViewController {
     }
 
     override func presentationAnimationDidFinish() {
-        if dbHelper.currentDatabaseLocation == dbHelper.sharedDbFile {
-            dcContext.logger = self.logger
-            dcContext.openDatabase(dbFile: dbHelper.sharedDbFile)
-            isAccountConfigured = dcContext.isConfigured()
-            if isAccountConfigured {
-                if #available(iOSApplicationExtension 13.0, *) {
-                   if let intent = self.extensionContext?.intent as? INSendMessageIntent, let chatId = Int(intent.conversationIdentifier ?? "") {
-                       selectedChatId = chatId
-                   }
-                }
+        dcAccounts.openDatabase()
+        dcContext.logger = self.logger
+        isAccountConfigured = dcContext.isConfigured()
+        if isAccountConfigured {
+            if #available(iOSApplicationExtension 13.0, *) {
+               if let intent = self.extensionContext?.intent as? INSendMessageIntent, let chatId = Int(intent.conversationIdentifier ?? "") {
+                   selectedChatId = chatId
+               }
+            }
 
-                if selectedChatId == nil {
-                    selectedChatId = dcContext.getChatIdByContactId(contactId: Int(DC_CONTACT_ID_SELF))
-                }
-                if let chatId = selectedChatId {
-                    selectedChat = dcContext.getChat(chatId: chatId)
-                }
-                DispatchQueue.global(qos: .userInitiated).async { [weak self] in
-                    guard let self = self else { return }
-                    self.shareAttachment = ShareAttachment(dcContext: self.dcContext, inputItems: self.extensionContext?.inputItems, delegate: self)
-                }
+            if selectedChatId == nil {
+                selectedChatId = dcContext.getChatIdByContactId(contactId: Int(DC_CONTACT_ID_SELF))
+            }
+            if let chatId = selectedChatId {
+                selectedChat = dcContext.getChat(chatId: chatId)
+            }
+            DispatchQueue.global(qos: .userInitiated).async { [weak self] in
+                guard let self = self else { return }
+                self.shareAttachment = ShareAttachment(dcContext: self.dcContext, inputItems: self.extensionContext?.inputItems, delegate: self)
             }
             reloadConfigurationItems()
             validateContent()
@@ -146,9 +144,7 @@ class ShareViewController: SLComposeServiceViewController {
     }
 
     func quit() {
-        if dbHelper.currentDatabaseLocation == dbHelper.sharedDbFile {
-            dcContext.closeDatabase()
-        }
+        dcAccounts.closeDatabase()
 
         // Inform the host that we're done, so it un-blocks its UI.
         self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)

+ 46 - 27
deltachat-ios/AppDelegate.swift

@@ -11,7 +11,7 @@ let logger = SwiftyBeaver.self
 
 @UIApplicationMain
 class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
-    private let dcContext = DcContext()
+    private let dcAccounts = DcAccounts()
     var appCoordinator: AppCoordinator!
     var relayHelper: RelayHelper!
     var locationManager: LocationManager!
@@ -55,7 +55,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
         let console = ConsoleDestination()
         console.format = "$DHH:mm:ss.SSS$d $C$L$c $M" // see https://docs.swiftybeaver.com/article/20-custom-format
         logger.addDestination(console)
-        dcContext.logger = DcLogger()
+
+        dcAccounts.openDatabase()
+        migrateToDcAccounts()
+        if dcAccounts.getAll().isEmpty, dcAccounts.add() == 0 {
+           fatalError("Could not initialize a new account.")
+        }
+        dcAccounts.getSelected().logger = DcLogger()
         logger.info("➡️ didFinishLaunchingWithOptions")
 
         window = UIWindow(frame: UIScreen.main.bounds)
@@ -68,14 +74,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
             window.backgroundColor = UIColor.white
         }
 
-        openDatabase()
         installEventHandler()
-        RelayHelper.setup(dcContext)
-        appCoordinator = AppCoordinator(window: window, dcContext: dcContext)
-        locationManager = LocationManager(context: dcContext)
+        relayHelper = RelayHelper.setup(dcAccounts.getSelected())
+        appCoordinator = AppCoordinator(window: window, dcAccounts: dcAccounts)
+        locationManager = LocationManager(dcAccounts: dcAccounts)
         UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum)
-        notificationManager = NotificationManager(dcContext: dcContext)
-        dcContext.maybeStartIo()
+        notificationManager = NotificationManager(dcAccounts: dcAccounts)
+        dcAccounts.maybeStartIo()
         setStockTranslations()
 
         reachability.whenReachable = { reachability in
@@ -83,7 +88,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
             // Reachability::reachabilityChanged uses DispatchQueue.main.async only
             logger.info("network: reachable", reachability.connection.description)
             DispatchQueue.global(qos: .background).async { [weak self] in
-                self?.dcContext.maybeNetwork()
+                self?.dcAccounts.maybeNetwork()
             }
         }
 
@@ -102,7 +107,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
             increaseDebugCounter("notify-remote-launch")
         }
 
-        if dcContext.isConfigured() && !UserDefaults.standard.bool(forKey: "notifications_disabled") {
+        if dcAccounts.getSelected().isConfigured() && !UserDefaults.standard.bool(forKey: "notifications_disabled") {
             registerForNotifications()
         }
 
@@ -138,12 +143,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
 
     func applicationWillEnterForeground(_: UIApplication) {
         logger.info("➡️ applicationWillEnterForeground")
-        dcContext.maybeStartIo()
+        dcAccounts.maybeStartIo()
 
         DispatchQueue.global(qos: .background).async { [weak self] in
             guard let self = self else { return }
             if self.reachability.connection != .none {
-                self.dcContext.maybeNetwork()
+                self.dcAccounts.maybeNetwork()
             }
 
             if let userDefaults = UserDefaults.shared, userDefaults.bool(forKey: UserDefaults.hasExtensionAttemptedToSend) {
@@ -170,7 +175,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
 
     func applicationWillTerminate(_: UIApplication) {
         logger.info("⬅️ applicationWillTerminate")
-        closeDatabase()
+        dcAccounts.closeDatabase()
         reachability.stopNotifier()
     }
 
@@ -206,7 +211,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
                 self.unregisterBackgroundTask()
             } else if app.backgroundTimeRemaining < 10 {
                 logger.info("⬅️ few background time, \(app.backgroundTimeRemaining), stopping")
-                self.dcContext.stopIo()
+                self.dcAccounts.stopIo()
 
                 // to avoid 0xdead10cc exceptions, scheduled jobs need to be done before we get suspended;
                 // we increase the probabilty that this happens by waiting a moment before calling unregisterBackgroundTask()
@@ -381,7 +386,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
         backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
             // usually, this handler is not used as we are taking care of timings below.
             logger.info("⬅️ finishing fetch by system urgency requests")
-            self?.dcContext.stopIo()
+            self?.dcAccounts.stopIo()
             completionHandler(.newData)
             if backgroundTask != .invalid {
                 UIApplication.shared.endBackgroundTask(backgroundTask)
@@ -390,8 +395,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
         }
 
         // we're in background, run IO for a little time
-        dcContext.maybeStartIo()
-        dcContext.maybeNetwork()
+        dcAccounts.maybeStartIo()
+        dcAccounts.maybeNetwork()
 
         DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in
             logger.info("⬅️ finishing fetch")
@@ -401,7 +406,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
                 return
             }
             if !self.appIsInForeground() {
-                self.dcContext.stopIo()
+                self.dcAccounts.stopIo()
             }
 
             // to avoid 0xdead10cc exceptions, scheduled jobs need to be done before we get suspended;
@@ -445,24 +450,37 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
 
     // MARK: - misc.
 
-    func openDatabase() {
-        guard let databaseLocation = DatabaseHelper(dcContext: dcContext).updateDatabaseLocation() else {
-            fatalError("Database could not be opened")
+    func migrateToDcAccounts() {
+        let dbHelper = DatabaseHelper()
+        if let databaseLocation = dbHelper.unmanagedDatabaseLocation,
+           dcAccounts.migrate(dbLocation: databaseLocation) == 0 {
+                fatalError("Account could not be migrated")
+                // TODO: show error message in UI
+        }
+    }
+
+    func reloadDcContext() {
+        locationManager.reloadDcContext()
+        notificationManager.reloadDcContext()
+        RelayHelper.sharedInstance.cancel()
+        _ = RelayHelper.setup(dcAccounts.getSelected())
+        if dcAccounts.getSelected().isConfigured() {
+            appCoordinator.resetTabBarRootViewControllers()
+        } else {
+            appCoordinator.presentWelcomeController()
         }
-        logger.info("open: \(databaseLocation)")
-        dcContext.openDatabase(dbFile: databaseLocation)
     }
 
-    func closeDatabase() {
-        dcContext.closeDatabase()
+    func deleteCurrentAccount() {
+        _ = dcAccounts.remove(id: dcAccounts.getSelected().id)
     }
 
     func installEventHandler() {
 
         DispatchQueue.global(qos: .background).async { [weak self] in
             guard let self = self else { return }
-            let eventHandler = DcEventHandler(dcContext: self.dcContext)
-            let eventEmitter = self.dcContext.getEventEmitter()
+            let eventHandler = DcEventHandler(dcAccounts: self.dcAccounts)
+            let eventEmitter = self.dcAccounts.getEventEmitter()
             while true {
                 guard let event = eventEmitter.getNextEvent() else { break }
                 eventHandler.handleEvent(event: event)
@@ -488,6 +506,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
     }
 
     private func setStockTranslations() {
+        let dcContext = dcAccounts.getSelected()
         dcContext.setStockTranslation(id: DC_STR_NOMESSAGES, localizationKey: "chat_no_messages")
         dcContext.setStockTranslation(id: DC_STR_SELF, localizationKey: "self")
         dcContext.setStockTranslation(id: DC_STR_DRAFT, localizationKey: "draft")

+ 29 - 20
deltachat-ios/Controller/AccountSetupController.swift

@@ -3,7 +3,8 @@ import UIKit
 import DcCore
 
 class AccountSetupController: UITableViewController, ProgressAlertHandler {
-    private let dcContext: DcContext
+    private var dcContext: DcContext
+    private let dcAccounts: DcAccounts
     private var skipOauth = false
     private var backupProgressObserver: NSObjectProtocol?
     var progressObserver: NSObjectProtocol?
@@ -278,9 +279,9 @@ class AccountSetupController: UITableViewController, ProgressAlertHandler {
             textLabel: String.localized("pref_watch_inbox_folder"),
             on: dcContext.getConfigBool("inbox_watch"),
             action: { cell in
-                self.dcContext.stopIo()
+                self.dcAccounts.stopIo()
                 self.dcContext.setConfigBool("inbox_watch", cell.isOn)
-                self.dcContext.maybeStartIo()
+                self.dcAccounts.maybeStartIo()
         })
     }()
 
@@ -289,9 +290,9 @@ class AccountSetupController: UITableViewController, ProgressAlertHandler {
             textLabel: String.localized("pref_watch_sent_folder"),
             on: dcContext.getConfigBool("sentbox_watch"),
             action: { cell in
-                self.dcContext.stopIo()
+                self.dcAccounts.stopIo()
                 self.dcContext.setConfigBool("sentbox_watch", cell.isOn)
-                self.dcContext.maybeStartIo()
+                self.dcAccounts.maybeStartIo()
         })
     }()
 
@@ -300,9 +301,9 @@ class AccountSetupController: UITableViewController, ProgressAlertHandler {
             textLabel: String.localized("pref_watch_mvbox_folder"),
             on: dcContext.getConfigBool("mvbox_watch"),
             action: { cell in
-                self.dcContext.stopIo()
+                self.dcAccounts.stopIo()
                 self.dcContext.setConfigBool("mvbox_watch", cell.isOn)
-                self.dcContext.maybeStartIo()
+                self.dcAccounts.maybeStartIo()
         })
     }()
 
@@ -335,9 +336,10 @@ class AccountSetupController: UITableViewController, ProgressAlertHandler {
     }()
 
     // MARK: - constructor
-    init(dcContext: DcContext, editView: Bool) {
+    init(dcAccounts: DcAccounts, editView: Bool) {
         self.editView = editView
-        self.dcContext = dcContext
+        self.dcAccounts = dcAccounts
+        self.dcContext = dcAccounts.getSelected()
 
         self.sections.append(basicSection)
         self.sections.append(advancedSection)
@@ -587,7 +589,7 @@ class AccountSetupController: UITableViewController, ProgressAlertHandler {
         }
 
         print("oAuth-Flag when loggin in: \(dcContext.getAuthFlags())")
-        dcContext.stopIo()
+        dcAccounts.stopIo()
         dcContext.configure()
         showProgressAlert(title: String.localized("login_header"), dcContext: dcContext)
     }
@@ -668,7 +670,7 @@ class AccountSetupController: UITableViewController, ProgressAlertHandler {
             notification in
             if let ui = notification.userInfo {
                 if ui["error"] as! Bool {
-                    self.dcContext.maybeStartIo()
+                    self.dcAccounts.maybeStartIo()
                     var errorMessage = ui["errorMessage"] as? String
                     if let appDelegate = UIApplication.shared.delegate as? AppDelegate, appDelegate.reachability.connection == .none {
                         errorMessage = String.localized("login_error_no_internet_connection")
@@ -677,7 +679,7 @@ class AccountSetupController: UITableViewController, ProgressAlertHandler {
                     }
                     self.updateProgressAlert(error: errorMessage)
                 } else if ui["done"] as! Bool {
-                    self.dcContext.maybeStartIo()
+                    self.dcAccounts.maybeStartIo()
                     self.updateProgressAlertSuccess(completion: self.handleLoginSuccess)
                 } else {
                     self.updateProgressAlertValue(value: ui["progress"] as? Int)
@@ -756,9 +758,10 @@ class AccountSetupController: UITableViewController, ProgressAlertHandler {
                 alert.addAction(UIAlertAction(title: String.localized("ok"), style: .cancel))
                 present(alert, animated: true)
             }
+        } else {
+            logger.error("no documents directory found")
         }
 
-        logger.error("no documents directory found")
     }
 
     private func deleteAccount() {
@@ -771,14 +774,20 @@ class AccountSetupController: UITableViewController, ProgressAlertHandler {
             message: nil,
             preferredStyle: .safeActionSheet)
 
-        alert.addAction(UIAlertAction(title: String.localized("delete_account"), style: .destructive, handler: { _ in
-            self.dcContext.stopIo()
-            appDelegate.closeDatabase()
-            DatabaseHelper(dcContext: self.dcContext).clearAccountData()
-            appDelegate.openDatabase()
+        alert.addAction(UIAlertAction(title: String.localized("delete_account"), style: .destructive, handler: { [weak self] _ in
+            guard let self = self else { return }
+            appDelegate.locationManager.disableLocationStreamingInAllChats()
+            self.dcAccounts.stopIo()
+            _ = self.dcAccounts.remove(id: self.dcAccounts.getSelected().id)
+            if let firstAccountId = self.dcAccounts.getAll().first {
+                _ = self.dcAccounts.select(id: firstAccountId)
+            } else {
+                let accountId = self.dcAccounts.add()
+                _ = self.dcAccounts.select(id: accountId)
+            }
+            appDelegate.reloadDcContext()
             appDelegate.installEventHandler()
-            self.dcContext.maybeStartIo()
-            appDelegate.appCoordinator.presentWelcomeController()
+            self.dcAccounts.maybeStartIo()
         }))
         alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel))
         present(alert, animated: true, completion: nil)

+ 5 - 3
deltachat-ios/Controller/EditSettingsController.swift

@@ -3,6 +3,7 @@ import DcCore
 
 class EditSettingsController: UITableViewController, MediaPickerDelegate {
     private let dcContext: DcContext
+    private let dcAccounts: DcAccounts
 
     private let section1 = 0
     private let section1Name = 0
@@ -51,8 +52,9 @@ class EditSettingsController: UITableViewController, MediaPickerDelegate {
         return cell
     }()
 
-    init(dcContext: DcContext) {
-        self.dcContext = dcContext
+    init(dcAccounts: DcAccounts) {
+        self.dcAccounts = dcAccounts
+        self.dcContext = dcAccounts.getSelected()
         super.init(style: .grouped)
         hidesBottomBarWhenPushed = true
     }
@@ -118,7 +120,7 @@ class EditSettingsController: UITableViewController, MediaPickerDelegate {
         if cell.tag == tagAccountSettingsCell {
             tableView.deselectRow(at: indexPath, animated: false)
             guard let nc = navigationController else { return }
-            let accountSetupVC = AccountSetupController(dcContext: dcContext, editView: true)
+            let accountSetupVC = AccountSetupController(dcAccounts: dcAccounts, editView: true)
             nc.pushViewController(accountSetupVC, animated: true)
         }
     }

+ 7 - 5
deltachat-ios/Controller/SettingsController.swift

@@ -27,6 +27,7 @@ internal final class SettingsViewController: UITableViewController, ProgressAler
     }
 
     private var dcContext: DcContext
+    private let dcAccounts: DcAccounts
 
     private let externalPathDescr = "File Sharing/Delta Chat"
 
@@ -206,8 +207,9 @@ internal final class SettingsViewController: UITableViewController, ProgressAler
         return [profileSection, preferencesSection, autocryptSection, backupSection, helpSection]
     }()
 
-    init(dcContext: DcContext) {
-        self.dcContext = dcContext
+    init(dcAccounts: DcAccounts) {
+        self.dcContext = dcAccounts.getSelected()
+        self.dcAccounts = dcAccounts
         super.init(style: .grouped)
     }
 
@@ -232,7 +234,7 @@ internal final class SettingsViewController: UITableViewController, ProgressAler
 
     override func viewDidAppear(_ animated: Bool) {
         super.viewDidAppear(animated)
-        addProgressAlertListener(dcContext: dcContext, progressName: dcNotificationImexProgress) { [weak self] in
+        addProgressAlertListener(dcAccounts: dcAccounts, progressName: dcNotificationImexProgress) { [weak self] in
             guard let self = self else { return }
             self.progressAlert?.dismiss(animated: true, completion: nil)
         }
@@ -423,7 +425,7 @@ internal final class SettingsViewController: UITableViewController, ProgressAler
         if !documents.isEmpty {
             showProgressAlert(title: String.localized("imex_progress_title_desktop"), dcContext: dcContext)
             DispatchQueue.main.async {
-                self.dcContext.stopIo()
+                self.dcAccounts.stopIo()
                 self.dcContext.imex(what: what, directory: documents[0])
             }
         } else {
@@ -441,7 +443,7 @@ internal final class SettingsViewController: UITableViewController, ProgressAler
 
     // MARK: - coordinator
     private func showEditSettingsController() {
-        let editController = EditSettingsController(dcContext: dcContext)
+        let editController = EditSettingsController(dcAccounts: dcAccounts)
         navigationController?.pushViewController(editController, animated: true)
     }
 

+ 15 - 8
deltachat-ios/Controller/WelcomeViewController.swift

@@ -2,7 +2,8 @@ import UIKit
 import DcCore
 
 class WelcomeViewController: UIViewController, ProgressAlertHandler {
-    private let dcContext: DcContext
+    private var dcContext: DcContext
+    private let dcAccounts: DcAccounts
     var progressObserver: NSObjectProtocol?
     var onProgressSuccess: VoidFunction?
 
@@ -16,7 +17,7 @@ class WelcomeViewController: UIViewController, ProgressAlertHandler {
         let view = WelcomeContentView()
         view.onLogin = { [weak self] in
             guard let self = self else { return }
-            let accountSetupController = AccountSetupController(dcContext: self.dcContext, editView: false)
+            let accountSetupController = AccountSetupController(dcAccounts: self.dcAccounts, editView: false)
             accountSetupController.onLoginSuccess = {
                 [weak self] in
                 if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
@@ -40,18 +41,20 @@ class WelcomeViewController: UIViewController, ProgressAlertHandler {
     private var qrCodeReader: QrCodeReaderController?
     weak var progressAlert: UIAlertController?
 
-    init(dcContext: DcContext) {
-        self.dcContext = dcContext
+    init(dcAccounts: DcAccounts) {
+        self.dcAccounts = dcAccounts
+        self.dcContext = dcAccounts.getSelected()
         super.init(nibName: nil, bundle: nil)
         self.navigationItem.title = String.localized("welcome_desktop")
         onProgressSuccess = { [weak self] in
-            let profileInfoController = ProfileInfoViewController(context: dcContext)
+            guard let self = self else { return }
+            let profileInfoController = ProfileInfoViewController(context: self.dcContext)
             profileInfoController.onClose = {
                 if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
                     appDelegate.appCoordinator.presentTabBarController()
                 }
             }
-            self?.navigationController?.setViewControllers([profileInfoController], animated: true)
+            self.navigationController?.setViewControllers([profileInfoController], animated: true)
         }
     }
 
@@ -113,9 +116,13 @@ class WelcomeViewController: UIViewController, ProgressAlertHandler {
     private func createAccountFromQRCode(qrCode: String) {
         let success = dcContext.setConfigFromQR(qrCode: qrCode)
         if success {
-            addProgressAlertListener(dcContext: dcContext, progressName: dcNotificationConfigureProgress, onSuccess: handleLoginSuccess)
+            addProgressAlertListener(dcAccounts: dcAccounts, progressName: dcNotificationConfigureProgress, onSuccess: handleLoginSuccess)
             showProgressAlert(title: String.localized("login_header"), dcContext: dcContext)
-            dcContext.stopIo()
+            dcAccounts.stopIo()
+            let accountId = dcAccounts.add()
+            if accountId != 0 {
+                self.dcContext = dcAccounts.get(id: accountId)
+            }
             dcContext.configure()
         } else {
             accountCreationErrorAlert()

+ 49 - 29
deltachat-ios/Coordinator/AppCoordinator.swift

@@ -7,7 +7,7 @@ import DcCore
 class AppCoordinator {
 
     private let window: UIWindow
-    private let dcContext: DcContext
+    private let dcAccounts: DcAccounts
     private let qrTab = 0
     public  let chatsTab = 1
     private let settingsTab = 2
@@ -21,7 +21,10 @@ class AppCoordinator {
     }()
 
     // MARK: - tabbar view handling
-    private lazy var tabBarController: UITabBarController = {
+    lazy var tabBarController: UITabBarController = {
+        let qrNavController = createQrNavigationController()
+        let chatsNavController = createChatsNavigationController()
+        let settingsNavController = createSettingsNavigationController()
         let tabBarController = UITabBarController()
         tabBarController.delegate = appStateRestorer
         tabBarController.viewControllers = [qrNavController, chatsNavController, settingsNavController]
@@ -29,41 +32,37 @@ class AppCoordinator {
         return tabBarController
     }()
 
-    private lazy var qrNavController: UINavigationController = {
-        let root = QrPageController(dcContext: dcContext)
+    private func createQrNavigationController() -> UINavigationController {
+        let root = QrPageController(dcContext: dcAccounts.getSelected())
         let nav = UINavigationController(rootViewController: root)
         let settingsImage = UIImage(named: "qr_code")
         nav.tabBarItem = UITabBarItem(title: String.localized("qr_code"), image: settingsImage, tag: qrTab)
         return nav
-    }()
+    }
 
-    private lazy var chatsNavController: UINavigationController = {
-        let viewModel = ChatListViewModel(dcContext: dcContext, isArchive: false)
-        let root = ChatListController(dcContext: dcContext, viewModel: viewModel)
+    private func createChatsNavigationController() -> UINavigationController {
+        let viewModel = ChatListViewModel(dcContext: dcAccounts.getSelected(), isArchive: false)
+        let root = ChatListController(dcContext: dcAccounts.getSelected(), viewModel: viewModel)
         let nav = UINavigationController(rootViewController: root)
         let settingsImage = UIImage(named: "ic_chat")
         nav.tabBarItem = UITabBarItem(title: String.localized("pref_chats"), image: settingsImage, tag: chatsTab)
         return nav
-    }()
+    }
 
-    private lazy var settingsNavController: UINavigationController = {
-        let root = SettingsViewController(dcContext: dcContext)
+    private func createSettingsNavigationController() -> UINavigationController {
+        let root = SettingsViewController(dcAccounts: dcAccounts)
         let nav = UINavigationController(rootViewController: root)
         let settingsImage = UIImage(named: "settings")
         nav.tabBarItem = UITabBarItem(title: String.localized("menu_settings"), image: settingsImage, tag: settingsTab)
         return nav
-    }()
+    }
 
     // MARK: - misc
-    init(window: UIWindow, dcContext: DcContext) {
+    init(window: UIWindow, dcAccounts: DcAccounts) {
         self.window = window
-        self.dcContext = dcContext
-
-        if dcContext.isConfigured() {
-            presentTabBarController()
-        } else {
-            presentWelcomeController()
-        }
+        self.dcAccounts = dcAccounts
+        let dcContext = dcAccounts.getSelected()
+        initializeRootController()
 
         let lastActiveTab = appStateRestorer.restoreLastActiveTab()
         if lastActiveTab == -1 {
@@ -87,31 +86,43 @@ class AppCoordinator {
 
     func showChat(chatId: Int, msgId: Int? = nil, animated: Bool = true, clearViewControllerStack: Bool = false) {
         showTab(index: chatsTab)
-        if let rootController = self.chatsNavController.viewControllers.first as? ChatListController {
+
+        if let rootController = self.tabBarController.selectedViewController as? UINavigationController {
             if clearViewControllerStack {
-                self.chatsNavController.popToRootViewController(animated: false)
+                rootController.popToRootViewController(animated: false)
+            }
+            if let controller = rootController.viewControllers.first as? ChatListController {
+                controller.showChat(chatId: chatId, highlightedMsg: msgId, animated: animated)
             }
-            rootController.showChat(chatId: chatId, highlightedMsg: msgId, animated: animated)
         }
     }
 
     func handleQRCode(_ code: String) {
         showTab(index: qrTab)
-        if let topViewController = qrNavController.topViewController,
+        if let navController = self.tabBarController.selectedViewController as? UINavigationController,
+           let topViewController = navController.topViewController,
             let qrPageController = topViewController as? QrPageController {
             qrPageController.handleQrCode(code)
         }
     }
 
+    func initializeRootController() {
+        if dcAccounts.getSelected().isConfigured() {
+            presentTabBarController()
+        } else {
+            presentWelcomeController()
+        }
+    }
+
     func presentWelcomeController() {
-        loginNavController.setViewControllers([WelcomeViewController(dcContext: dcContext)], animated: true)
+        loginNavController.setViewControllers([WelcomeViewController(dcAccounts: dcAccounts)], animated: true)
         window.rootViewController = loginNavController
         window.makeKeyAndVisible()
 
         // the applicationIconBadgeNumber is remembered by the system even on reinstalls (just tested on ios 13.3.1),
         // to avoid appearing an old number of a previous installation, we reset the counter manually.
         // but even when this changes in ios, we need the reset as we allow account-deletion also in-app.
-        NotificationManager.updateApplicationIconBadge(dcContext: dcContext, reset: true)
+        NotificationManager.updateApplicationIconBadge(dcContext: dcAccounts.getSelected(), reset: true)
     }
 
     func presentTabBarController() {
@@ -121,8 +132,17 @@ class AppCoordinator {
     }
 
     func popTabsToRootViewControllers() {
-        qrNavController.popToRootViewController(animated: false)
-        chatsNavController.popToRootViewController(animated: false)
-        settingsNavController.popToRootViewController(animated: false)
+        self.tabBarController.viewControllers?.forEach { controller in
+            if let navController = controller as? UINavigationController {
+                navController.popToRootViewController(animated: false)
+            }
+        }
+    }
+
+    func resetTabBarRootViewControllers() {
+        self.tabBarController.setViewControllers([createQrNavigationController(),
+                                                  createChatsNavigationController(),
+                                                  createSettingsNavigationController()], animated: false)
+        presentTabBarController()
     }
 }

+ 4 - 4
deltachat-ios/Handler/ProgressAlertHandler.swift

@@ -8,7 +8,7 @@ protocol ProgressAlertHandler: UIViewController {
     func updateProgressAlertValue(value: Int?)
     func updateProgressAlert(error: String?)
     func updateProgressAlertSuccess(completion: VoidFunction?)
-    func addProgressAlertListener(dcContext: DcContext, progressName: Notification.Name, onSuccess: @escaping VoidFunction)
+    func addProgressAlertListener(dcAccounts: DcAccounts, progressName: Notification.Name, onSuccess: @escaping VoidFunction)
 }
 
 extension ProgressAlertHandler {
@@ -63,7 +63,7 @@ extension ProgressAlertHandler {
         })
     }
 
-    func addProgressAlertListener(dcContext: DcContext, progressName: Notification.Name, onSuccess: @escaping VoidFunction) {
+    func addProgressAlertListener(dcAccounts: DcAccounts, progressName: Notification.Name, onSuccess: @escaping VoidFunction) {
         let nc = NotificationCenter.default
         progressObserver = nc.addObserver(
             forName: progressName,
@@ -73,10 +73,10 @@ extension ProgressAlertHandler {
             guard let self = self else { return }
             if let ui = notification.userInfo {
                 if ui["error"] as? Bool ?? false {
-                    dcContext.maybeStartIo()
+                    dcAccounts.maybeStartIo()
                     self.updateProgressAlert(error: ui["errorMessage"] as? String)
                 } else if ui["done"] as? Bool ?? false {
-                    dcContext.maybeStartIo()
+                    dcAccounts.maybeStartIo()
                     self.updateProgressAlertSuccess(completion: onSuccess)
                 } else {
                     self.updateProgressAlertValue(value: ui["progress"] as? Int)

+ 9 - 3
deltachat-ios/Helper/LocationManager.swift

@@ -5,13 +5,15 @@ import DcCore
 class LocationManager: NSObject, CLLocationManagerDelegate {
 
     let locationManager: CLLocationManager
-    let dcContext: DcContext
+    let dcAccounts: DcAccounts
+    var dcContext: DcContext
     var lastLocation: CLLocation?
     var chatIdLocationRequest: Int?
     var durationLocationRequest: Int?
 
-    init(context: DcContext) {
-        dcContext = context
+    init(dcAccounts: DcAccounts) {
+        self.dcAccounts = dcAccounts
+        self.dcContext = dcAccounts.getSelected()
         locationManager = CLLocationManager()
         locationManager.distanceFilter = 25
         locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
@@ -23,6 +25,10 @@ class LocationManager: NSObject, CLLocationManagerDelegate {
 
     }
 
+    public func reloadDcContext() {
+        dcContext = dcAccounts.getSelected()
+    }
+
     func shareLocation(chatId: Int, duration: Int) {
         if duration > 0 {
             var authStatus: CLAuthorizationStatus

+ 9 - 2
deltachat-ios/Helper/NotificationManager.swift

@@ -8,14 +8,21 @@ public class NotificationManager {
     var incomingMsgObserver: NSObjectProtocol?
     var msgsNoticedObserver: NSObjectProtocol?
 
+    private let dcAccounts: DcAccounts
     private var dcContext: DcContext
 
-    init(dcContext: DcContext) {
-        self.dcContext = dcContext
+    init(dcAccounts: DcAccounts) {
+        self.dcAccounts = dcAccounts
+        self.dcContext = dcAccounts.getSelected()
         initIncomingMsgsObserver()
         initMsgsNoticedObserver()
     }
 
+    public func reloadDcContext() {
+        NotificationManager.removeAllNotifications()
+        dcContext = dcAccounts.getSelected()
+    }
+
     public static func updateApplicationIconBadge(dcContext: DcContext, reset: Bool) {
         var unreadMessages = 0
         if !reset {

+ 2 - 1
deltachat-ios/Helper/RelayHelper.swift

@@ -12,8 +12,9 @@ class RelayHelper {
         }
     }
 
-    class func setup(_ dcContext: DcContext) {
+    class func setup(_ dcContext: DcContext) -> RelayHelper {
         RelayHelper.dcContext = dcContext
+        return sharedInstance
     }
 
     func setForwardMessage(messageId: Int) {