USB-Meter / USB Meter / AppDelegate.swift
1 contributor
297 lines | 10.973kb
//
//  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<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])
    }
}