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