USB-Meter / USB Meter / AppDelegate.swift
1 contributor
247 lines | 8.872kb
//
//  AppDelegate.swift
//  USB Meter
//
//  Created by Bogdan Timofte on 01/03/2020.
//  Copyright © 2020 Bogdan Timofte. All rights reserved.
//

import CloudKit
import CoreData
import UIKit
import UserNotifications

//let btSerial = BluetoothSerial(delegate: BSD())
let appData = AppData()
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)")
}

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])
    }
}