// // AppDelegate.swift // USB Meter // // Created by Bogdan Timofte on 01/03/2020. // Copyright © 2020 Bogdan Timofte. All rights reserved. // import CloudKit import CoreData import OSLog import UIKit import UserNotifications //let btSerial = BluetoothSerial(delegate: BSD()) let appData = AppData() private let restoreLogger = Logger(subsystem: "ro.xdev.USB-Meter", category: "Restore") private enum DebugLogFlag: String { case all = "USB_METER_DEBUG_LOGS" case bluetooth = "USB_METER_BLUETOOTH_LOGS" case cloud = "USB_METER_CLOUD_LOGS" case meter = "USB_METER_METER_LOGS" case migration = "USB_METER_MIGRATION_LOGS" case notifications = "USB_METER_NOTIFICATION_LOGS" case restore = "USB_METER_RESTORE_LOGS" case sync = "USB_METER_SYNC_LOGS" } public func debugLogFlagEnabled(_ flag: String) -> Bool { ProcessInfo.processInfo.environment[DebugLogFlag.all.rawValue] == "1" || ProcessInfo.processInfo.environment[flag] == "1" } enum Constants { static let chartUnderscan: CGFloat = 0.5 static let chartOverscan: CGFloat = 1 - chartUnderscan } // MARK: Clock // MARK: Debug public func track(_ message: String = "", file: String = #file, function: String = #function, line: Int = #line ) { guard shouldEmitTrackMessage(message, file: file, function: function) else { return } let date = Date() let calendar = Calendar.current let hour = calendar.component(.hour, from: date) let minutes = calendar.component(.minute, from: date) let seconds = calendar.component(.second, from: date) print("\(hour):\(minutes):\(seconds) - \(file):\(line) - \(function) \(message)") } public func restoreTrace(_ message: String) { guard debugLogFlagEnabled(DebugLogFlag.restore.rawValue) else { return } restoreLogger.debug("\(message, privacy: .public)") } private func shouldEmitTrackMessage(_ message: String, file: String, function: String) -> Bool { #if DEBUG if debugLogFlagEnabled(DebugLogFlag.all.rawValue) { return true } let importantMarkers = [ "Error", "error", "Failed", "failed", "timeout", "Timeout", "Missing", "missing", "overflow", "Disconnect", "disconnect", "Disconnected", "unauthorized", "not authorized", "not supported", "Unexpected", "Invalid Context", "This is not possible!", "Buffer overflow" ] if importantMarkers.contains(where: { message.contains($0) }) { return true } if isTrackMessageEnabledByCategory(message, file: file, function: function) { return true } return false #else _ = message _ = file _ = function return false #endif } private func isTrackMessageEnabledByCategory(_ message: String, file: String, function: String) -> Bool { let categories = debugLogCategories(for: message, file: file, function: function) return categories.contains { debugLogFlagEnabled($0.rawValue) } } private func debugLogCategories(for message: String, file: String, function: String) -> [DebugLogFlag] { var categories = [DebugLogFlag]() if file.contains("Bluetooth") || function.contains("centralManager") || function.contains("peripheral(") || message.contains("Bluetooth") || message.contains("BLE discovery") || message.contains("peripheral") || message.contains("characteristic") || message.contains("service") { categories.append(.bluetooth) } if file.contains("Meter.swift") || message.contains("data request") || message.contains("Operational state") || message.contains("recordingThreshold") || message.contains("screenTimeout") || message.contains("screenBrightness") || message.contains("volatile memory") || message.contains("charger type") { categories.append(.meter) } if message.contains("CloudKit") || message.contains("iCloud") || message.contains("ubiquityIdentityToken") { categories.append(.cloud) } if file.contains("MeterNameStore") || file.contains("ChargerStandbyPowerStore") || message.contains("KVS") || message.contains("ubiquitous") { categories.append(.sync) } if file.contains("ChargeInsightsStore") || message.contains("promoted legacy") || message.contains("synthesized custom") || message.contains("healed duplicate") { categories.append(.migration) } if message.contains("notification") || message.contains("remote notifications") { categories.append(.notifications) } return categories } @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { private let cloudKitContainerIdentifier = "iCloud.ro.xdev.USB-Meter" func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { logRuntimeICloudDiagnostics() UNUserNotificationCenter.current().delegate = self application.registerForRemoteNotifications() appData.activateChargeInsights(context: persistentContainer.viewContext) configureNavigationBarAppearance() return true } private func configureNavigationBarAppearance() { let titleFont = UIFont.systemFont(ofSize: 19, weight: .semibold) let appearance = UINavigationBarAppearance() appearance.configureWithDefaultBackground() appearance.titleTextAttributes = [.font: titleFont] UINavigationBar.appearance().standardAppearance = appearance UINavigationBar.appearance().scrollEdgeAppearance = appearance UINavigationBar.appearance().compactAppearance = appearance } private func logRuntimeICloudDiagnostics() { #if DEBUG guard debugLogFlagEnabled(DebugLogFlag.cloud.rawValue) else { return } let hasUbiquityIdentityToken = FileManager.default.ubiquityIdentityToken != nil track("Runtime iCloud diagnostics: ubiquityIdentityTokenAvailable=\(hasUbiquityIdentityToken)") CKContainer(identifier: cloudKitContainerIdentifier).accountStatus { status, error in if let error { track("CloudKit account status error: \(error.localizedDescription)") return } track("CloudKit account status: \(status.rawValue)") } #endif } // MARK: UISceneSession Lifecycle func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { // Called when the user discards a scene session. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } func applicationWillTerminate(_ application: UIApplication) { _ = appData.flushChargeInsights() saveContext() } func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { #if DEBUG track("Registered for remote notifications with device token length \(deviceToken.count)") #endif } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { track("Remote notification registration failed: \(error.localizedDescription)") } func application( _ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void ) { #if DEBUG track("Received remote notification with keys: \(userInfo.keys.map(String.init(describing:)).joined(separator: ", "))") #endif completionHandler(.newData) } lazy var persistentContainer: NSPersistentCloudKitContainer = { let container = NSPersistentCloudKitContainer(name: "CKModel") if let description = container.persistentStoreDescriptions.first { description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions( containerIdentifier: cloudKitContainerIdentifier ) description.shouldMigrateStoreAutomatically = true description.shouldInferMappingModelAutomatically = true description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) } container.loadPersistentStores { storeDescription, error in if let error = error as NSError? { // Log the error but do NOT destroy the store — wiping local data and // waiting for a full CloudKit re-sync is far worse than a degraded launch. NSLog( "Core Data store load failed (url=%@): %@ — %@", storeDescription.url?.path ?? "unknown", error.localizedDescription, error.userInfo ) #if DEBUG fatalError("Unresolved Core Data error \(error), \(error.userInfo)") #endif } } container.viewContext.automaticallyMergesChangesFromParent = true container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy return container }() func saveContext() { let context = persistentContainer.viewContext guard context.hasChanges else { return } do { try context.save() } catch { let nsError = error as NSError NSLog("Core Data save failed: %@", nsError.localizedDescription) #if DEBUG fatalError("Unresolved Core Data save error \(nsError), \(nsError.userInfo)") #endif } } func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { completionHandler([.banner, .sound, .list]) } }