// // AppDelegate.swift // SimpleX // // Created by Evgeny on 30/03/2022. // Copyright © 2022 SimpleX Chat. All rights reserved. // import Foundation import UIKit import SimpleXChat import SwiftUI class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { logger.debug("AppDelegate: didFinishLaunchingWithOptions") application.registerForRemoteNotifications() removePasscodesIfReinstalled() prepareForLaunch() return true } func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let token = deviceToken.map { String(format: "%02hhx", $0) }.joined() logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)") let m = ChatModel.shared let deviceToken = DeviceToken(pushProvider: PushProvider(env: pushEnvironment), token: token) m.deviceToken = deviceToken // savedToken is set in startChat, when it is started before this method is called if m.savedToken != nil { registerToken(token: deviceToken) } } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { logger.error("AppDelegate: didFailToRegisterForRemoteNotificationsWithError \(error.localizedDescription)") } func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { logger.debug("AppDelegate: didReceiveRemoteNotification") let m = ChatModel.shared if let ntfData = userInfo["notificationData"] as? [AnyHashable : Any], m.notificationMode != .off { if let verification = ntfData["verification"] as? String, let nonce = ntfData["nonce"] as? String { if let token = m.deviceToken { logger.debug("AppDelegate: didReceiveRemoteNotification: verification, confirming \(verification)") Task { do { if case .active = m.tokenStatus {} else { m.tokenStatus = .confirmed } try await apiVerifyToken(token: token, nonce: nonce, code: verification) m.tokenStatus = .active } catch { if let cr = error as? ChatResponse, case .chatCmdError(_, .errorAgent(.NTF(.AUTH))) = cr { m.tokenStatus = .expired } logger.error("AppDelegate: didReceiveRemoteNotification: apiVerifyToken or apiIntervalNofication error: \(responseError(error))") } completionHandler(.newData) } } else { completionHandler(.noData) } } else if let checkMessages = ntfData["checkMessages"] as? Bool, checkMessages { logger.debug("AppDelegate: didReceiveRemoteNotification: checkMessages") if m.ntfEnablePeriodic && allowBackgroundRefresh() && BGManager.shared.lastRanLongAgo { receiveMessages(completionHandler) } else { completionHandler(.noData) } } else { completionHandler(.noData) } } else { completionHandler(.noData) } } func applicationWillTerminate(_ application: UIApplication) { logger.debug("DEBUGGING: AppDelegate: applicationWillTerminate") ChatModel.shared.filesToDelete.forEach { removeFile($0) } ChatModel.shared.filesToDelete = [] terminateChat() } func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) if connectingSceneSession.role == .windowApplication { configuration.delegateClass = SceneDelegate.self } return configuration } private func receiveMessages(_ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { let complete = BGManager.shared.completionHandler { logger.debug("AppDelegate: completed BGManager.receiveMessages") completionHandler(.newData) } BGManager.shared.receiveMessages(complete) } private func removePasscodesIfReinstalled() { // Check for the database existence, because app and self destruct passcodes // will be saved and restored by iOS when a user deletes and re-installs the app. // In this case the database and settings will be deleted, but the passcodes won't be. // Deleting passcodes ensures that the user will not get stuck on "Opening app..." screen. if (kcAppPassword.get() != nil || kcSelfDestructPassword.get() != nil) && !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) && !hasDatabase() { _ = kcAppPassword.remove() _ = kcSelfDestructPassword.remove() _ = kcDatabasePassword.remove() } } private func prepareForLaunch() { try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) } static func keepScreenOn(_ on: Bool) { UIApplication.shared.isIdleTimerDisabled = on } } class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate { var window: UIWindow? static var windowStatic: UIWindow? var windowScene: UIWindowScene? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { UITableView.appearance().backgroundColor = .clear guard let windowScene = scene as? UIWindowScene else { return } self.windowScene = windowScene window = windowScene.keyWindow SceneDelegate.windowStatic = windowScene.keyWindow migrateAccentColorAndTheme() ThemeManager.applyTheme(currentThemeDefault.get()) ThemeManager.adjustWindowStyle() } private func migrateAccentColorAndTheme() { let defs = UserDefaults.standard /// For checking migration // themeOverridesDefault.set([]) // currentThemeDefault.set(DefaultTheme.SYSTEM_THEME_NAME) // defs.set(0.5, forKey: DEFAULT_ACCENT_COLOR_RED) // defs.set(0.3, forKey: DEFAULT_ACCENT_COLOR_GREEN) // defs.set(0.8, forKey: DEFAULT_ACCENT_COLOR_BLUE) let userInterfaceStyle = getUserInterfaceStyleDefault() if defs.double(forKey: DEFAULT_ACCENT_COLOR_GREEN) == 0 && userInterfaceStyle == .unspecified { // No migration needed or already migrated return } let defaultAccentColor = Color(cgColor: CGColor(red: 0.000, green: 0.533, blue: 1.000, alpha: 1)) let accentColor = Color(cgColor: getUIAccentColorDefault()) if accentColor != defaultAccentColor { let colors = ThemeColors(primary: accentColor.toReadableHex()) var overrides = themeOverridesDefault.get() var themeIds = currentThemeIdsDefault.get() switch userInterfaceStyle { case .light: let light = ThemeOverrides(base: DefaultTheme.LIGHT, colors: colors, wallpaper: ThemeWallpaper(preset: PresetWallpaper.school.filename)) overrides.append(light) themeOverridesDefault.set(overrides) themeIds[DefaultTheme.LIGHT.themeName] = light.themeId currentThemeIdsDefault.set(themeIds) ThemeManager.applyTheme(DefaultTheme.LIGHT.themeName) case .dark: let dark = ThemeOverrides(base: DefaultTheme.DARK, colors: colors, wallpaper: ThemeWallpaper(preset: PresetWallpaper.school.filename)) overrides.append(dark) themeOverridesDefault.set(overrides) themeIds[DefaultTheme.DARK.themeName] = dark.themeId currentThemeIdsDefault.set(themeIds) ThemeManager.applyTheme(DefaultTheme.DARK.themeName) case .unspecified: let light = ThemeOverrides(base: DefaultTheme.LIGHT, colors: colors, wallpaper: ThemeWallpaper(preset: PresetWallpaper.school.filename)) let dark = ThemeOverrides(base: DefaultTheme.DARK, colors: colors, wallpaper: ThemeWallpaper(preset: PresetWallpaper.school.filename)) overrides.append(light) overrides.append(dark) themeOverridesDefault.set(overrides) themeIds[DefaultTheme.LIGHT.themeName] = light.themeId themeIds[DefaultTheme.DARK.themeName] = dark.themeId currentThemeIdsDefault.set(themeIds) ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME) @unknown default: () } } else if userInterfaceStyle != .unspecified { let themeName = switch userInterfaceStyle { case .light: DefaultTheme.LIGHT.themeName case .dark: DefaultTheme.DARK.themeName default: DefaultTheme.SYSTEM_THEME_NAME } ThemeManager.applyTheme(themeName) } defs.removeObject(forKey: DEFAULT_ACCENT_COLOR_RED) defs.removeObject(forKey: DEFAULT_ACCENT_COLOR_GREEN) defs.removeObject(forKey: DEFAULT_ACCENT_COLOR_BLUE) defs.removeObject(forKey: DEFAULT_USER_INTERFACE_STYLE) } }