1 contributor
//
// 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")
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) {
restoreLogger.debug("\(message, privacy: .public)")
}
private func shouldEmitTrackMessage(_ message: String, file: String, function: String) -> Bool {
#if DEBUG
if ProcessInfo.processInfo.environment["USB_METER_VERBOSE_LOGS"] == "1" {
return true
}
#if targetEnvironment(macCatalyst)
let importantMarkers = [
"Error",
"error",
"Failed",
"failed",
"timeout",
"Timeout",
"Missing",
"missing",
"overflow",
"Disconnect",
"disconnect",
"Disconnected",
"unauthorized",
"not authorized",
"not supported",
"Unexpected",
"Invalid Context",
"ignored",
"Guard:",
"Skip data request",
"Dropping unsolicited data",
"This is not possible!",
"Inferred",
"Clearing",
"Reconnecting"
]
if importantMarkers.contains(where: { message.contains($0) }) {
return true
}
let noisyFunctions: Set<String> = [
"logRuntimeICloudDiagnostics()",
"refreshCloudAvailability(reason:)",
"start()",
"centralManagerDidUpdateState(_:)",
"discoveredMeter(peripheral:advertising:rssi:)",
"connect()",
"connectionEstablished()",
"peripheral(_:didDiscoverServices:)",
"peripheral(_:didDiscoverCharacteristicsFor:error:)",
"refreshOperationalStateIfReady()",
"peripheral(_:didUpdateNotificationStateFor:error:)",
"scheduleDataDumpRequest(after:reason:)"
]
if noisyFunctions.contains(function) {
return false
}
let noisyMarkers = [
"Runtime iCloud diagnostics",
"iCloud availability",
"Starting Bluetooth manager",
"Bluetooth is On... Start scanning...",
"adding new USB Meter",
"Connect called for",
"Connection established for",
"Optional([<CBService:",
"Optional([<CBCharacteristic:",
"Waiting for notifications on",
"Notification state updated for",
"Peripheral ready with notify",
"Schedule data request in",
"Operational state changed"
]
if noisyMarkers.contains(where: { message.contains($0) }) {
return false
}
#endif
return true
#else
_ = file
_ = function
return false
#endif
}
@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)
return true
}
private func logRuntimeICloudDiagnostics() {
#if DEBUG
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<UISceneSession>) {
// 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])
}
}