123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770 |
- import AudioToolbox
- import Reachability
- import UIKit
- import UserNotifications
- import DcCore
- import SDWebImageWebPCoder
- import Intents
- import SDWebImageSVGKitPlugin
- let logger = SimpleLogger()
- @UIApplicationMain
- class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
- private let dcAccounts = DcAccounts()
- var appCoordinator: AppCoordinator!
- var relayHelper: RelayHelper!
- var locationManager: LocationManager!
- var notificationManager: NotificationManager!
- private var backgroundTask: UIBackgroundTaskIdentifier = .invalid
- var reachability: Reachability?
- var window: UIWindow?
- var notifyToken: String?
- var applicationInForeground: Bool = false
- private var launchOptions: [UIApplication.LaunchOptionsKey: Any]?
- private var appFullyInitialized = false
- // purpose of `bgIoTimestamp` is to block rapidly subsequent calls to remote- or local-wakeups:
- //
- // `bgIoTimestamp` is set to enter-background or last remote- or local-wakeup;
- // in the minute after these events, subsequent remote- or local-wakeups are skipped
- // in favor to the chance of being awakened when it makes more sense
- // and to avoid issues with calling concurrent series of startIo/maybeNetwork/stopIo.
- private var bgIoTimestamp: Double = 0.0
- // MARK: - app main entry point
- // `didFinishLaunchingWithOptions` is the main entry point
- // that is called if the app is started for the first time
- // or after the app is killed.
- //
- // - `didFinishLaunchingWithOptions` is also called before event methods as `didReceiveRemoteNotification` are called -
- // either _directly before_ (if the app was killed) or _long before_ (if the app was suspended).
- //
- // - in some cases `didFinishLaunchingWithOptions` is called _instead_ an event method and `launchOptions` tells the reason;
- // the event method may or may not be called in this case, see #1542 for some deeper information.
- //
- // `didFinishLaunchingWithOptions` is _not_ called
- // when the app wakes up from "suspended" state
- // (app is in memory in the background but no code is executed, IO stopped)
- func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
- // explicitly ignore SIGPIPE to avoid crashes, see https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/NetworkingOverview/CommonPitfalls/CommonPitfalls.html
- // setupCrashReporting() may create an additional handler, but we do not want to rely on that
- signal(SIGPIPE, SIG_IGN)
- logger.info("➡️ didFinishLaunchingWithOptions")
- let webPCoder = SDImageWebPCoder.shared
- SDImageCodersManager.shared.addCoder(webPCoder)
- let svgCoder = SDImageSVGKCoder.shared
- SDImageCodersManager.shared.addCoder(svgCoder)
- dcAccounts.logger = SimpleLogger()
- dcAccounts.openDatabase()
- migrateToDcAccounts()
- self.launchOptions = launchOptions
- continueDidFinishLaunchingWithOptions()
- return true
- }
- // finishes the app initialization which depends on the successful access to the keychain
- func continueDidFinishLaunchingWithOptions() {
- if let sharedUserDefaults = UserDefaults.shared, !sharedUserDefaults.bool(forKey: UserDefaults.hasSavedKeyToKeychain) {
- // we can assume a fresh install (UserDefaults are deleted on app removal)
- // -> reset the keychain (which survives removals of the app) in case the app was removed and reinstalled.
- if !KeychainManager.deleteDBSecrets() {
- logger.warning("Failed to delete DB secrets")
- }
- }
-
- do {
- self.reachability = try Reachability()
- } catch {
- // TODO: Handle
- }
- let accountIds = dcAccounts.getAll()
- for accountId in accountIds {
- let dcContext = dcAccounts.get(id: accountId)
- if !dcContext.isOpen() {
- do {
- let secret = try KeychainManager.getAccountSecret(accountID: accountId)
- if !dcContext.open(passphrase: secret) {
- logger.error("Failed to open database for account \(accountId)")
- }
- } catch KeychainError.accessError(let message, let status) {
- logger.error("Keychain error. \(message). Error status: \(status)")
- return
- } catch KeychainError.unhandledError(let message, let status) {
- logger.error("Keychain error. \(message). Error status: \(status)")
- } catch {
- logger.error("\(error)")
- }
- }
- }
- if dcAccounts.getAll().isEmpty, dcAccounts.add() == 0 {
- fatalError("Could not initialize a new account.")
- }
- window = UIWindow(frame: UIScreen.main.bounds)
- guard let window = window else {
- fatalError("window was nil in app delegate")
- }
- if #available(iOS 13.0, *) {
- window.backgroundColor = UIColor.systemBackground
- } else {
- window.backgroundColor = UIColor.white
- }
- installEventHandler()
- relayHelper = RelayHelper.setup(dcAccounts.getSelected())
- appCoordinator = AppCoordinator(window: window, dcAccounts: dcAccounts)
- locationManager = LocationManager(dcAccounts: dcAccounts)
- UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum)
- notificationManager = NotificationManager(dcAccounts: dcAccounts)
- setStockTranslations()
- dcAccounts.startIo()
- if let reachability = reachability {
- reachability.whenReachable = { reachability in
- // maybeNetwork() shall not be called in ui thread;
- // Reachability::reachabilityChanged uses DispatchQueue.main.async only
- logger.info("network: reachable", reachability.connection.description)
- DispatchQueue.global(qos: .background).async { [weak self] in
- guard let self = self else { return }
- self.dcAccounts.maybeNetwork()
- if self.notifyToken == nil &&
- self.dcAccounts.getSelected().isConfigured() &&
- !UserDefaults.standard.bool(forKey: "notifications_disabled") {
- self.registerForNotifications()
- }
- }
- }
-
- reachability.whenUnreachable = { _ in
- logger.info("network: not reachable")
- DispatchQueue.global(qos: .background).async { [weak self] in
- self?.dcAccounts.maybeNetworkLost()
- }
- }
-
- do {
- try reachability.startNotifier()
- } catch {
- logger.error("Unable to start notifier")
- }
- }
-
- if let notificationOption = launchOptions?[.remoteNotification] {
- logger.info("Notifications: remoteNotification: \(String(describing: notificationOption))")
- increaseDebugCounter("notify-remote-launch")
- pushToDebugArray("📡'")
- performFetch(completionHandler: { (_) -> Void in })
- }
- if dcAccounts.getSelected().isConfigured() && !UserDefaults.standard.bool(forKey: "notifications_disabled") {
- registerForNotifications()
- }
- launchOptions = nil
- appFullyInitialized = true
- }
- // `open` is called when an url should be opened by Delta Chat.
- // we currently use that for handling oauth2 and for handing openpgp4fpr.
- //
- // before `open` gets called, `didFinishLaunchingWithOptions` is called.
- func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
- logger.info("➡️ open url")
- // gets here when app returns from oAuth2-Setup process - the url contains the provided token
- // if let params = url.queryParameters, let token = params["code"] {
- // NotificationCenter.default.post(name: NSNotification.Name("oauthLoginApproved"), object: nil, userInfo: ["token": token])
- // }
- switch url.scheme?.lowercased() {
- case "dcaccount", "dclogin":
- appCoordinator.handleQRCode(url.absoluteString)
- return true
- case "openpgp4fpr":
- // Hack to format url properly
- let urlString = url.absoluteString
- .replacingOccurrences(of: "openpgp4fpr", with: "OPENPGP4FPR", options: .literal, range: nil)
- .replacingOccurrences(of: "%23", with: "#", options: .literal, range: nil)
- appCoordinator.handleQRCode(urlString)
- return true
- case "mailto":
- return appCoordinator.handleMailtoURL(url)
- case "chat.delta.deeplink":
- return appCoordinator.handleDeepLinkURL(url)
- default:
- return false
- }
- }
- // MARK: - app lifecycle
- // applicationWillEnterForeground() is _not_ called on initial app start
- func applicationWillEnterForeground(_: UIApplication) {
- logger.info("➡️ applicationWillEnterForeground")
- applicationInForeground = true
- dcAccounts.startIo()
- DispatchQueue.global(qos: .background).async { [weak self] in
- guard let self = self else { return }
- if let reachability = self.reachability {
- if reachability.connection != .unavailable {
- self.dcAccounts.maybeNetwork()
- }
- }
- AppDelegate.emitMsgsChangedIfShareExtensionWasUsed()
- }
- }
- func applicationProtectedDataDidBecomeAvailable(_ application: UIApplication) {
- logger.info("➡️ applicationProtectedDataDidBecomeAvailable")
- if !appFullyInitialized {
- continueDidFinishLaunchingWithOptions()
- }
- }
- func applicationProtectedDataWillBecomeUnavailable(_ application: UIApplication) {
- logger.info("⬅️ applicationProtectedDataWillBecomeUnavailable")
- }
- static func emitMsgsChangedIfShareExtensionWasUsed() {
- if let userDefaults = UserDefaults.shared, userDefaults.bool(forKey: UserDefaults.hasExtensionAttemptedToSend) {
- userDefaults.removeObject(forKey: UserDefaults.hasExtensionAttemptedToSend)
- DispatchQueue.main.async {
- NotificationCenter.default.post(
- name: dcNotificationChanged,
- object: nil,
- userInfo: [
- "message_id": Int(0),
- "chat_id": Int(0),
- ]
- )
- }
- }
- }
- // applicationDidBecomeActive() is called on initial app start _and_ after applicationWillEnterForeground()
- func applicationDidBecomeActive(_: UIApplication) {
- logger.info("➡️ applicationDidBecomeActive")
- applicationInForeground = true
- }
- func applicationWillResignActive(_: UIApplication) {
- logger.info("⬅️ applicationWillResignActive")
- registerBackgroundTask()
- }
- func applicationDidEnterBackground(_: UIApplication) {
- logger.info("⬅️ applicationDidEnterBackground")
- applicationInForeground = false
- }
- func applicationWillTerminate(_: UIApplication) {
- logger.info("⬅️ applicationWillTerminate")
- dcAccounts.closeDatabase()
- if let reachability = reachability {
- reachability.stopNotifier()
- }
- }
- // MARK: - fade out app smoothly
- // let the app run in background for a little while
- // eg. to complete sending messages out and to react to responses.
- private func registerBackgroundTask() {
- logger.info("⬅️ registering background task")
- bgIoTimestamp = Double(Date().timeIntervalSince1970)
- unregisterBackgroundTask()
- backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
- // usually, the background thread is finished before in maybeStop()
- logger.info("⬅️ background expirationHandler called")
- self?.unregisterBackgroundTask()
- }
- maybeStop()
- }
- private func unregisterBackgroundTask() {
- if backgroundTask != .invalid {
- UIApplication.shared.endBackgroundTask(backgroundTask)
- backgroundTask = .invalid
- }
- }
- private func maybeStop() {
- DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
- let app = UIApplication.shared
- if app.applicationState != .background {
- logger.info("⬅️ no longer in background")
- self.unregisterBackgroundTask()
- } else if app.backgroundTimeRemaining < 10 {
- logger.info("⬅️ few background time, \(app.backgroundTimeRemaining), stopping")
- 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()
- DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
- logger.info("⬅️ few background time, \(app.backgroundTimeRemaining), done")
- self.unregisterBackgroundTask()
- }
- } else {
- logger.info("⬅️ remaining background time: \(app.backgroundTimeRemaining)")
- self.maybeStop()
- }
- }
- }
- // MARK: - background fetch and notifications
- // `registerForNotifications` asks the user if they want to get notifiations shown.
- // if so, it registers for receiving remote notifications.
- func registerForNotifications() {
- UNUserNotificationCenter.current().delegate = self
- notifyToken = nil
- // register for showing notifications
- //
- // note: the alert-dialog cannot be customized, however, since iOS 12,
- // it can be avoided completely by using `.provisional`,
- // https://developer.apple.com/documentation/usernotifications/asking_permission_to_use_notifications
- UNUserNotificationCenter.current()
- .requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, _ in
- if granted {
- // we are allowed to show notifications:
- // register for receiving remote notifications
- logger.info("Notifications: Permission granted: \(granted)")
- self?.maybeRegisterForRemoteNotifications()
- } else {
- logger.info("Notifications: Permission not granted.")
- }
- }
- }
- // register on apple server for receiving remote notifications
- // and pass the token to the app's notification server.
- //
- // on success, we get a token at didRegisterForRemoteNotificationsWithDeviceToken;
- // on failure, didFailToRegisterForRemoteNotificationsWithError is called
- private func maybeRegisterForRemoteNotifications() {
- UNUserNotificationCenter.current().getNotificationSettings { settings in
- logger.info("Notifications: Settings: \(settings)")
- switch settings.authorizationStatus {
- case .authorized, .provisional, .ephemeral:
- DispatchQueue.main.async {
- UIApplication.shared.registerForRemoteNotifications()
- }
- case .denied, .notDetermined:
- break
- }
- }
- }
- // `didRegisterForRemoteNotificationsWithDeviceToken` is called by iOS
- // when the call to `UIApplication.shared.registerForRemoteNotifications` succeeded.
- //
- // we pass the received token to the app's notification server then.
- func application(
- _ application: UIApplication,
- didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
- ) {
- let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
- let tokenString = tokenParts.joined()
- let endpoint = "https://notifications.delta.chat/register"
- logger.info("Notifications: POST token: \(tokenString) to \(endpoint)")
- if let url = URL(string: endpoint) {
- var request = URLRequest(url: url)
- request.httpMethod = "POST"
- let body = "{ \"token\": \"\(tokenString)\" }"
- request.httpBody = body.data(using: String.Encoding.utf8)
- let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
- if let error = error {
- logger.error("Notifications: cannot POST to notification server: \(error)")
- return
- }
- if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode == 200 {
- logger.info("Notifications: request to notification server succeeded")
- } else {
- logger.error("Notifications: request to notification server failed: \(String(describing: response)), \(String(describing: data))")
- }
- self.notifyToken = tokenString
- }
- task.resume()
- } else {
- logger.error("Notifications: cannot create URL for token: \(tokenString)")
- }
- }
- // `didFailToRegisterForRemoteNotificationsWithError` is called by iOS
- // when the call to `UIApplication.shared.registerForRemoteNotifications` failed.
- func application(
- _ application: UIApplication,
- didFailToRegisterForRemoteNotificationsWithError error: Error) {
- logger.error("Notifications: Failed to register: \(error)")
- }
- // `didReceiveRemoteNotification` is called by iOS when a remote notification is received.
- //
- // we need to ensure IO is running as the function may be called from suspended state
- // (with app in memory, but gracefully shut down before; sort of freezed).
- // if the function was not called from suspended state,
- // the call to startIo() did nothing, therefore, interrupt and force fetch.
- //
- // we have max. 30 seconds time for our job and to call the completion handler.
- // as the system tracks the elapsed time, power usage, and data costs, we return faster,
- // after 10 seconds, things should be done.
- // (see https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623013-application)
- // (at some point it would be nice if we get a clear signal from the core)
- func application(
- _ application: UIApplication,
- didReceiveRemoteNotification userInfo: [AnyHashable: Any],
- fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
- ) {
- logger.info("➡️ Notifications: didReceiveRemoteNotification \(userInfo)")
- increaseDebugCounter("notify-remote-receive")
- pushToDebugArray("📡")
- performFetch(completionHandler: completionHandler)
- }
- // `performFetchWithCompletionHandler` is called by iOS on local wakeup.
- //
- // this requires "UIBackgroundModes: fetch" to be set in Info.plist
- // ("App downloads content from the network" in Xcode)
- //
- // we have 30 seconds time for our job, things are quite similar as in `didReceiveRemoteNotification`
- func application(
- _ application: UIApplication,
- performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
- ) {
- logger.info("➡️ Notifications: performFetchWithCompletionHandler")
- increaseDebugCounter("notify-local-wakeup")
- pushToDebugArray("🏠")
- performFetch(completionHandler: completionHandler)
- }
- private func performFetch(completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
- // `didReceiveRemoteNotification` as well as `performFetchWithCompletionHandler` might be called if we're in foreground,
- // in this case, there is no need to wait for things or do sth.
- if appIsInForeground() {
- logger.info("➡️ app already in foreground")
- pushToDebugArray("OK1")
- completionHandler(.newData)
- return
- }
- // from time to time, `didReceiveRemoteNotification` and `performFetchWithCompletionHandler`
- // are actually called at the same millisecond.
- //
- // therefore, if last fetch is less than a minute ago, we skip this call;
- // this also lets the completionHandler being called earlier so that we maybe get awakened when it makes more sense.
- //
- // nb: calling the completion handler with .noData results in less calls overall.
- // if at some point we do per-message-push-notifications, we need to tweak this gate.
- let nowTimestamp = Double(Date().timeIntervalSince1970)
- if nowTimestamp < bgIoTimestamp + 60 {
- logger.info("➡️ fetch was just executed, skipping")
- pushToDebugArray("OK2")
- completionHandler(.newData)
- return
- }
- bgIoTimestamp = nowTimestamp
- // make sure to balance each call to `beginBackgroundTask` with `endBackgroundTask`
- var backgroundTask: UIBackgroundTaskIdentifier = .invalid
- 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?.pushToDebugArray("ERR1")
- self?.dcAccounts.stopIo()
- completionHandler(.newData)
- if backgroundTask != .invalid {
- UIApplication.shared.endBackgroundTask(backgroundTask)
- backgroundTask = .invalid
- }
- }
- pushToDebugArray("1")
- // move work to non-main thread to not block UI (otherwise, in case we get suspended, the app is blocked totally)
- // (we are using `qos: default` as `qos: .background` or `main.asyncAfter` may be delayed by tens of minutes)
- DispatchQueue.global().async { [weak self] in
- guard let self = self else { completionHandler(.failed); return }
- // we're in background, run IO for a little time
- self.dcAccounts.startIo()
- self.dcAccounts.maybeNetwork()
- self.pushToDebugArray("2")
- self.addDebugFetchTimestamp()
- // create a new semaphore to make sure the received DC_CONNECTIVITY_CONNECTED really belongs to maybeNetwork() from above
- // (maybeNetwork() sets connectivity to DC_CONNECTIVITY_CONNECTING, when fetch is done, we're back at DC_CONNECTIVITY_CONNECTED)
- self.dcAccounts.fetchSemaphore = DispatchSemaphore(value: 0)
- _ = self.dcAccounts.fetchSemaphore?.wait(timeout: .now() + 20)
- self.dcAccounts.fetchSemaphore = nil
- // TOCHECK: it seems, we are not always reaching this point in code,
- // semaphore?.wait() does not always exit after the given timeout and the app gets suspended -
- // maybe that is on purpose somehow to suspend inactive apps, not sure.
- // this does not happen often, but still.
- // cmp. https://github.com/deltachat/deltachat-ios/pull/1542#pullrequestreview-951620906
- logger.info("⬅️ finishing fetch")
- self.pushToDebugArray(String(format: "3/%.3fs", Double(Date().timeIntervalSince1970)-nowTimestamp))
- if !self.appIsInForeground() {
- 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 completionHandler()
- usleep(1_000_000)
- logger.info("⬅️ fetch done")
- completionHandler(.newData)
- if backgroundTask != .invalid {
- self.pushToDebugArray("OK3")
- UIApplication.shared.endBackgroundTask(backgroundTask)
- backgroundTask = .invalid
- }
- }
- }
- // MARK: - handle notification banners
- // This method will be called if an incoming message was received while the app was in foreground.
- // We don't show foreground notifications in the notification center because they don't get grouped properly
- func userNotificationCenter(_: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
- logger.info("Notifications: foreground notification")
- completionHandler([.badge])
- }
- // this method will be called if the user tapped on a notification
- func userNotificationCenter(_: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
- if !response.notification.request.identifier.containsExact(subSequence: Constants.notificationIdentifier).isEmpty {
- logger.info("Notifications: notification tapped")
- let userInfo = response.notification.request.content.userInfo
- if let chatId = userInfo["chat_id"] as? Int,
- let msgId = userInfo["message_id"] as? Int {
- if !appCoordinator.isShowingChat(chatId: chatId) {
- appCoordinator.showChat(chatId: chatId, msgId: msgId, animated: false, clearViewControllerStack: true)
- }
- }
- }
- completionHandler()
- }
- // MARK: - misc.
- func migrateToDcAccounts() {
- let dbHelper = DatabaseHelper()
- if let databaseLocation = dbHelper.unmanagedDatabaseLocation {
- if dcAccounts.migrate(dbLocation: databaseLocation) == 0 {
- fatalError("Account could not be migrated")
- // TODO: show error message in UI
- }
- INInteraction.deleteAll(completion: nil)
- }
- }
- /// - Parameters:
- /// - accountCode: optional string representation of dcaccounts: url, used to setup a new account
- func reloadDcContext(accountCode: String? = nil) {
- setStockTranslations()
- locationManager.reloadDcContext()
- notificationManager.reloadDcContext()
- RelayHelper.shared.finishRelaying()
- _ = RelayHelper.setup(dcAccounts.getSelected())
- if dcAccounts.getSelected().isConfigured() {
- appCoordinator.resetTabBarRootViewControllers()
- } else {
- appCoordinator.presentWelcomeController(accountCode: accountCode)
- }
- }
- func installEventHandler() {
- DispatchQueue.global(qos: .background).async { [weak self] in
- guard let self = self else { return }
- let eventHandler = DcEventHandler(dcAccounts: self.dcAccounts)
- let eventEmitter = self.dcAccounts.getEventEmitter()
- while true {
- guard let event = eventEmitter.getNextEvent() else { break }
- eventHandler.handleEvent(event: event)
- }
- logger.info("⬅️ event emitter finished")
- }
- }
- // Values calculated for debug log view
- private func increaseDebugCounter(_ name: String) {
- let nowDate = Date()
- let nowTimestamp = Double(nowDate.timeIntervalSince1970)
- let startTimestamp = UserDefaults.standard.double(forKey: name + "-start")
- if nowTimestamp > startTimestamp + 60*60*24 {
- let cal: Calendar = Calendar(identifier: .gregorian)
- let newStartDate: Date = cal.date(bySettingHour: 0, minute: 0, second: 0, of: nowDate)!
- UserDefaults.standard.set(0, forKey: name + "-count")
- UserDefaults.standard.set(Double(newStartDate.timeIntervalSince1970), forKey: name + "-start")
- }
- let cnt = UserDefaults.standard.integer(forKey: name + "-count")
- UserDefaults.standard.set(cnt + 1, forKey: name + "-count")
- UserDefaults.standard.set(nowTimestamp, forKey: name + "-last")
- }
- // Values calculated for connectivity view
- private func addDebugFetchTimestamp() {
- let nowTimestamp = Double(Date().timeIntervalSince1970)
- let timestamps = UserDefaults.standard.array(forKey: Constants.Keys.notificationTimestamps)
- var slidingTimeframe: [Double]
- if timestamps != nil, let timestamps = timestamps as? [Double] {
- slidingTimeframe = timestamps.filter({ nowTimestamp < $0 + 60 * 60 * 24 })
- } else {
- slidingTimeframe = [Double]()
- }
- slidingTimeframe.append(nowTimestamp)
- UserDefaults.standard.set(slidingTimeframe, forKey: Constants.Keys.notificationTimestamps)
- }
- private func pushToDebugArray(_ value: String) {
- let name = "notify-fetch-info2"
- let values = UserDefaults.standard.array(forKey: name)
- var slidingValues = [String]()
- if values != nil, let values = values as? [String] {
- slidingValues = values.suffix(512)
- }
- slidingValues.append(value+"/"+DateUtils.getExtendedAbsTimeSpanString(timeStamp: Double(Date().timeIntervalSince1970)))
- UserDefaults.standard.set(slidingValues, forKey: name)
- }
- 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")
- dcContext.setStockTranslation(id: DC_STR_VOICEMESSAGE, localizationKey: "voice_message")
- dcContext.setStockTranslation(id: DC_STR_IMAGE, localizationKey: "image")
- dcContext.setStockTranslation(id: DC_STR_VIDEO, localizationKey: "video")
- dcContext.setStockTranslation(id: DC_STR_AUDIO, localizationKey: "audio")
- dcContext.setStockTranslation(id: DC_STR_FILE, localizationKey: "file")
- dcContext.setStockTranslation(id: DC_STR_GIF, localizationKey: "gif")
- dcContext.setStockTranslation(id: DC_STR_ENCRYPTEDMSG, localizationKey: "encrypted_message")
- dcContext.setStockTranslation(id: DC_STR_CANTDECRYPT_MSG_BODY, localizationKey: "systemmsg_cannot_decrypt")
- dcContext.setStockTranslation(id: DC_STR_READRCPT, localizationKey: "systemmsg_read_receipt_subject")
- dcContext.setStockTranslation(id: DC_STR_READRCPT_MAILBODY, localizationKey: "systemmsg_read_receipt_body")
- dcContext.setStockTranslation(id: DC_STR_CONTACT_VERIFIED, localizationKey: "contact_verified")
- dcContext.setStockTranslation(id: DC_STR_CONTACT_NOT_VERIFIED, localizationKey: "contact_not_verified")
- dcContext.setStockTranslation(id: DC_STR_CONTACT_SETUP_CHANGED, localizationKey: "contact_setup_changed")
- dcContext.setStockTranslation(id: DC_STR_ARCHIVEDCHATS, localizationKey: "chat_archived_label")
- dcContext.setStockTranslation(id: DC_STR_AC_SETUP_MSG_SUBJECT, localizationKey: "autocrypt_asm_subject")
- dcContext.setStockTranslation(id: DC_STR_AC_SETUP_MSG_BODY, localizationKey: "autocrypt_asm_general_body")
- dcContext.setStockTranslation(id: DC_STR_CANNOT_LOGIN, localizationKey: "login_error_cannot_login")
- dcContext.setStockTranslation(id: DC_STR_LOCATION, localizationKey: "location")
- dcContext.setStockTranslation(id: DC_STR_STICKER, localizationKey: "sticker")
- dcContext.setStockTranslation(id: DC_STR_DEVICE_MESSAGES, localizationKey: "device_talk")
- dcContext.setStockTranslation(id: DC_STR_SAVED_MESSAGES, localizationKey: "saved_messages")
- dcContext.setStockTranslation(id: DC_STR_DEVICE_MESSAGES_HINT, localizationKey: "device_talk_explain")
- dcContext.setStockTranslation(id: DC_STR_WELCOME_MESSAGE, localizationKey: "device_talk_welcome_message")
- dcContext.setStockTranslation(id: DC_STR_UNKNOWN_SENDER_FOR_CHAT, localizationKey: "systemmsg_unknown_sender_for_chat")
- dcContext.setStockTranslation(id: DC_STR_SUBJECT_FOR_NEW_CONTACT, localizationKey: "systemmsg_subject_for_new_contact")
- dcContext.setStockTranslation(id: DC_STR_FAILED_SENDING_TO, localizationKey: "systemmsg_failed_sending_to")
- dcContext.setStockTranslation(id: DC_STR_VIDEOCHAT_INVITATION, localizationKey: "videochat_invitation")
- dcContext.setStockTranslation(id: DC_STR_VIDEOCHAT_INVITE_MSG_BODY, localizationKey: "videochat_invitation_body")
- dcContext.setStockTranslation(id: DC_STR_CONFIGURATION_FAILED, localizationKey: "configuration_failed_with_error")
- dcContext.setStockTranslation(id: DC_STR_BAD_TIME_MSG_BODY, localizationKey: "devicemsg_bad_time")
- dcContext.setStockTranslation(id: DC_STR_UPDATE_REMINDER_MSG_BODY, localizationKey: "devicemsg_update_reminder")
- dcContext.setStockTranslation(id: DC_STR_REPLY_NOUN, localizationKey: "reply_noun")
- dcContext.setStockTranslation(id: DC_STR_SELF_DELETED_MSG_BODY, localizationKey: "devicemsg_self_deleted")
- dcContext.setStockTranslation(id: DC_STR_FORWARDED, localizationKey: "forwarded")
- dcContext.setStockTranslation(id: DC_STR_QUOTA_EXCEEDING_MSG_BODY, localizationKey: "devicemsg_storage_exceeding")
- dcContext.setStockTranslation(id: DC_STR_PARTIAL_DOWNLOAD_MSG_BODY, localizationKey: "n_bytes_message")
- dcContext.setStockTranslation(id: DC_STR_DOWNLOAD_AVAILABILITY, localizationKey: "download_max_available_until")
- dcContext.setStockTranslation(id: DC_STR_INCOMING_MESSAGES, localizationKey: "incoming_messages")
- dcContext.setStockTranslation(id: DC_STR_OUTGOING_MESSAGES, localizationKey: "outgoing_messages")
- dcContext.setStockTranslation(id: DC_STR_STORAGE_ON_DOMAIN, localizationKey: "storage_on_domain")
- dcContext.setStockTranslation(id: DC_STR_CONNECTED, localizationKey: "connectivity_connected")
- dcContext.setStockTranslation(id: DC_STR_CONNTECTING, localizationKey: "connectivity_connecting")
- dcContext.setStockTranslation(id: DC_STR_UPDATING, localizationKey: "connectivity_updating")
- dcContext.setStockTranslation(id: DC_STR_SENDING, localizationKey: "sending")
- dcContext.setStockTranslation(id: DC_STR_LAST_MSG_SENT_SUCCESSFULLY, localizationKey: "last_msg_sent_successfully")
- dcContext.setStockTranslation(id: DC_STR_ERROR, localizationKey: "error_x")
- dcContext.setStockTranslation(id: DC_STR_NOT_SUPPORTED_BY_PROVIDER, localizationKey: "not_supported_by_provider")
- dcContext.setStockTranslation(id: DC_STR_MESSAGES, localizationKey: "messages")
- dcContext.setStockTranslation(id: DC_STR_BROADCAST_LIST, localizationKey: "broadcast_list")
- dcContext.setStockTranslation(id: DC_STR_PART_OF_TOTAL_USED, localizationKey: "part_of_total_used")
- dcContext.setStockTranslation(id: DC_STR_SECURE_JOIN_STARTED, localizationKey: "secure_join_started")
- dcContext.setStockTranslation(id: DC_STR_SECURE_JOIN_REPLIES, localizationKey: "secure_join_replies")
- dcContext.setStockTranslation(id: DC_STR_SETUP_CONTACT_QR_DESC, localizationKey: "qrshow_join_contact_hint")
- dcContext.setStockTranslation(id: DC_STR_SECURE_JOIN_GROUP_QR_DESC, localizationKey: "qrshow_join_group_hint")
- dcContext.setStockTranslation(id: DC_STR_NOT_CONNECTED, localizationKey: "connectivity_not_connected")
- dcContext.setStockTranslation(id: DC_STR_AEAP_ADDR_CHANGED, localizationKey: "aeap_addr_changed")
- dcContext.setStockTranslation(id: DC_STR_AEAP_EXPLANATION_AND_LINK, localizationKey: "aeap_explanation")
- dcContext.setStockTranslation(id: DC_STR_GROUP_NAME_CHANGED_BY_YOU, localizationKey: "group_name_changed_by_you")
- dcContext.setStockTranslation(id: DC_STR_GROUP_NAME_CHANGED_BY_OTHER, localizationKey: "group_name_changed_by_other")
- dcContext.setStockTranslation(id: DC_STR_GROUP_IMAGE_CHANGED_BY_YOU, localizationKey: "group_image_changed_by_you")
- dcContext.setStockTranslation(id: DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER, localizationKey: "group_image_changed_by_other")
- dcContext.setStockTranslation(id: DC_STR_ADD_MEMBER_BY_YOU, localizationKey: "add_member_by_you")
- dcContext.setStockTranslation(id: DC_STR_ADD_MEMBER_BY_OTHER, localizationKey: "add_member_by_other")
- dcContext.setStockTranslation(id: DC_STR_REMOVE_MEMBER_BY_YOU, localizationKey: "remove_member_by_you")
- dcContext.setStockTranslation(id: DC_STR_REMOVE_MEMBER_BY_OTHER, localizationKey: "remove_member_by_other")
- dcContext.setStockTranslation(id: DC_STR_GROUP_LEFT_BY_YOU, localizationKey: "group_left_by_you")
- dcContext.setStockTranslation(id: DC_STR_GROUP_LEFT_BY_OTHER, localizationKey: "group_left_by_other")
- dcContext.setStockTranslation(id: DC_STR_GROUP_IMAGE_DELETED_BY_YOU, localizationKey: "group_image_deleted_by_you")
- dcContext.setStockTranslation(id: DC_STR_GROUP_IMAGE_DELETED_BY_OTHER, localizationKey: "group_image_deleted_by_other")
- dcContext.setStockTranslation(id: DC_STR_LOCATION_ENABLED_BY_YOU, localizationKey: "location_enabled_by_you")
- dcContext.setStockTranslation(id: DC_STR_LOCATION_ENABLED_BY_OTHER, localizationKey: "location_enabled_by_other")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU, localizationKey: "ephemeral_timer_disabled_by_you")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER, localizationKey: "ephemeral_timer_disabled_by_other")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU, localizationKey: "ephemeral_timer_seconds_by_you")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER, localizationKey: "ephemeral_timer_seconds_by_other")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU, localizationKey: "ephemeral_timer_1_minute_by_you")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER, localizationKey: "ephemeral_timer_1_minute_by_other")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU, localizationKey: "ephemeral_timer_1_hour_by_you")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER, localizationKey: "ephemeral_timer_1_hour_by_other")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU, localizationKey: "ephemeral_timer_1_day_by_you")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER, localizationKey: "ephemeral_timer_1_day_by_other")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU, localizationKey: "ephemeral_timer_1_week_by_you")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER, localizationKey: "ephemeral_timer_1_week_by_other")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU, localizationKey: "ephemeral_timer_minutes_by_you")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER, localizationKey: "ephemeral_timer_minutes_by_other")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU, localizationKey: "ephemeral_timer_hours_by_you")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER, localizationKey: "ephemeral_timer_hours_by_other")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU, localizationKey: "ephemeral_timer_days_by_you")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER, localizationKey: "ephemeral_timer_days_by_other")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU, localizationKey: "ephemeral_timer_weeks_by_you")
- dcContext.setStockTranslation(id: DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER, localizationKey: "ephemeral_timer_weeks_by_other")
- dcContext.setStockTranslation(id: DC_STR_PROTECTION_ENABLED_BY_YOU, localizationKey: "protection_enabled_by_you")
- dcContext.setStockTranslation(id: DC_STR_PROTECTION_ENABLED_BY_OTHER, localizationKey: "protection_enabled_by_other")
- dcContext.setStockTranslation(id: DC_STR_PROTECTION_DISABLED_BY_YOU, localizationKey: "protection_disabled_by_you")
- dcContext.setStockTranslation(id: DC_STR_PROTECTION_DISABLED_BY_OTHER, localizationKey: "protection_disabled_by_other")
- }
- func appIsInForeground() -> Bool {
- if Thread.isMainThread {
- switch UIApplication.shared.applicationState {
- case .background, .inactive:
- applicationInForeground = false
- case .active:
- applicationInForeground = true
- }
- }
- return applicationInForeground
- }
- }
|