USB-Meter / USB Meter / AppDelegate.swift
Newer Older
297 lines | 10.973kb
Bogdan Timofte authored 2 months ago
1
//
2
//  AppDelegate.swift
3
//  USB Meter
4
//
5
//  Created by Bogdan Timofte on 01/03/2020.
6
//  Copyright © 2020 Bogdan Timofte. All rights reserved.
7
//
8

            
Bogdan Timofte authored a month ago
9
import CloudKit
10
import CoreData
Bogdan Timofte authored a month ago
11
import OSLog
Bogdan Timofte authored 2 months ago
12
import UIKit
Bogdan Timofte authored a month ago
13
import UserNotifications
Bogdan Timofte authored 2 months ago
14

            
15
//let btSerial = BluetoothSerial(delegate: BSD())
16
let appData = AppData()
Bogdan Timofte authored a month ago
17
private let restoreLogger = Logger(subsystem: "ro.xdev.USB-Meter", category: "Restore")
Bogdan Timofte authored a month ago
18
private enum DebugLogFlag: String {
19
    case all = "USB_METER_DEBUG_LOGS"
20
    case bluetooth = "USB_METER_BLUETOOTH_LOGS"
21
    case cloud = "USB_METER_CLOUD_LOGS"
22
    case meter = "USB_METER_METER_LOGS"
23
    case migration = "USB_METER_MIGRATION_LOGS"
24
    case notifications = "USB_METER_NOTIFICATION_LOGS"
25
    case restore = "USB_METER_RESTORE_LOGS"
26
    case sync = "USB_METER_SYNC_LOGS"
27
}
28

            
29
public func debugLogFlagEnabled(_ flag: String) -> Bool {
30
    ProcessInfo.processInfo.environment[DebugLogFlag.all.rawValue] == "1" ||
31
    ProcessInfo.processInfo.environment[flag] == "1"
32
}
33

            
Bogdan Timofte authored 2 months ago
34
enum Constants {
35
    static let chartUnderscan: CGFloat = 0.5
36
    static let chartOverscan: CGFloat = 1 - chartUnderscan
37
}
38
// MARK: Clock
39

            
40
// MARK: Debug
41
public func track(_ message: String = "", file: String = #file, function: String = #function, line: Int = #line ) {
Bogdan Timofte authored 2 months ago
42
    guard shouldEmitTrackMessage(message, file: file, function: function) else {
43
        return
44
    }
Bogdan Timofte authored 2 months ago
45
    let date = Date()
46
    let calendar = Calendar.current
47
    let hour = calendar.component(.hour, from: date)
48
    let minutes = calendar.component(.minute, from: date)
49
    let seconds = calendar.component(.second, from: date)
50
    print("\(hour):\(minutes):\(seconds) - \(file):\(line) - \(function) \(message)")
51
}
52

            
Bogdan Timofte authored a month ago
53
public func restoreTrace(_ message: String) {
Bogdan Timofte authored a month ago
54
    guard debugLogFlagEnabled(DebugLogFlag.restore.rawValue) else { return }
Bogdan Timofte authored a month ago
55
    restoreLogger.debug("\(message, privacy: .public)")
56
}
57

            
Bogdan Timofte authored 2 months ago
58
private func shouldEmitTrackMessage(_ message: String, file: String, function: String) -> Bool {
59
    #if DEBUG
Bogdan Timofte authored a month ago
60
    if debugLogFlagEnabled(DebugLogFlag.all.rawValue) {
Bogdan Timofte authored 2 months ago
61
        return true
62
    }
63

            
64
    let importantMarkers = [
65
        "Error",
66
        "error",
67
        "Failed",
68
        "failed",
69
        "timeout",
70
        "Timeout",
71
        "Missing",
72
        "missing",
73
        "overflow",
74
        "Disconnect",
75
        "disconnect",
76
        "Disconnected",
77
        "unauthorized",
78
        "not authorized",
79
        "not supported",
80
        "Unexpected",
81
        "Invalid Context",
82
        "This is not possible!",
Bogdan Timofte authored a month ago
83
        "Buffer overflow"
Bogdan Timofte authored 2 months ago
84
    ]
85

            
86
    if importantMarkers.contains(where: { message.contains($0) }) {
87
        return true
88
    }
89

            
Bogdan Timofte authored a month ago
90
    if isTrackMessageEnabledByCategory(message, file: file, function: function) {
91
        return true
Bogdan Timofte authored 2 months ago
92
    }
93

            
Bogdan Timofte authored a month ago
94
    return false
Bogdan Timofte authored 2 months ago
95
    #else
Bogdan Timofte authored a month ago
96
    _ = message
Bogdan Timofte authored 2 months ago
97
    _ = file
98
    _ = function
99
    return false
100
    #endif
101
}
102

            
Bogdan Timofte authored a month ago
103
private func isTrackMessageEnabledByCategory(_ message: String, file: String, function: String) -> Bool {
104
    let categories = debugLogCategories(for: message, file: file, function: function)
105
    return categories.contains { debugLogFlagEnabled($0.rawValue) }
106
}
107

            
108
private func debugLogCategories(for message: String, file: String, function: String) -> [DebugLogFlag] {
109
    var categories = [DebugLogFlag]()
110

            
111
    if file.contains("Bluetooth") ||
112
        function.contains("centralManager") ||
113
        function.contains("peripheral(") ||
114
        message.contains("Bluetooth") ||
115
        message.contains("BLE discovery") ||
116
        message.contains("peripheral") ||
117
        message.contains("characteristic") ||
118
        message.contains("service") {
119
        categories.append(.bluetooth)
120
    }
121

            
122
    if file.contains("Meter.swift") ||
123
        message.contains("data request") ||
124
        message.contains("Operational state") ||
125
        message.contains("recordingThreshold") ||
126
        message.contains("screenTimeout") ||
127
        message.contains("screenBrightness") ||
128
        message.contains("volatile memory") ||
129
        message.contains("charger type") {
130
        categories.append(.meter)
131
    }
132

            
133
    if message.contains("CloudKit") ||
134
        message.contains("iCloud") ||
135
        message.contains("ubiquityIdentityToken") {
136
        categories.append(.cloud)
137
    }
138

            
139
    if file.contains("MeterNameStore") ||
140
        file.contains("ChargerStandbyPowerStore") ||
141
        message.contains("KVS") ||
142
        message.contains("ubiquitous") {
143
        categories.append(.sync)
144
    }
145

            
146
    if file.contains("ChargeInsightsStore") ||
147
        message.contains("promoted legacy") ||
148
        message.contains("synthesized custom") ||
149
        message.contains("healed duplicate") {
150
        categories.append(.migration)
151
    }
152

            
153
    if message.contains("notification") ||
154
        message.contains("remote notifications") {
155
        categories.append(.notifications)
156
    }
157

            
158
    return categories
159
}
160

            
Bogdan Timofte authored 2 months ago
161
@UIApplicationMain
Bogdan Timofte authored a month ago
162
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
163
    private let cloudKitContainerIdentifier = "iCloud.ro.xdev.USB-Meter"
Bogdan Timofte authored 2 months ago
164

            
165

            
166
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Bogdan Timofte authored 2 months ago
167
        logRuntimeICloudDiagnostics()
Bogdan Timofte authored a month ago
168
        UNUserNotificationCenter.current().delegate = self
169
        application.registerForRemoteNotifications()
170
        appData.activateChargeInsights(context: persistentContainer.viewContext)
Bogdan Timofte authored a month ago
171
        configureNavigationBarAppearance()
Bogdan Timofte authored 2 months ago
172
        return true
173
    }
174

            
Bogdan Timofte authored a month ago
175
    private func configureNavigationBarAppearance() {
176
        let titleFont = UIFont.systemFont(ofSize: 19, weight: .semibold)
177
        let appearance = UINavigationBarAppearance()
178
        appearance.configureWithDefaultBackground()
179
        appearance.titleTextAttributes = [.font: titleFont]
180
        UINavigationBar.appearance().standardAppearance = appearance
181
        UINavigationBar.appearance().scrollEdgeAppearance = appearance
182
        UINavigationBar.appearance().compactAppearance = appearance
183
    }
184

            
Bogdan Timofte authored 2 months ago
185
    private func logRuntimeICloudDiagnostics() {
186
        #if DEBUG
Bogdan Timofte authored a month ago
187
        guard debugLogFlagEnabled(DebugLogFlag.cloud.rawValue) else { return }
Bogdan Timofte authored 2 months ago
188
        let hasUbiquityIdentityToken = FileManager.default.ubiquityIdentityToken != nil
189
        track("Runtime iCloud diagnostics: ubiquityIdentityTokenAvailable=\(hasUbiquityIdentityToken)")
Bogdan Timofte authored a month ago
190
        CKContainer(identifier: cloudKitContainerIdentifier).accountStatus { status, error in
191
            if let error {
192
                track("CloudKit account status error: \(error.localizedDescription)")
193
                return
194
            }
195

            
196
            track("CloudKit account status: \(status.rawValue)")
197
        }
Bogdan Timofte authored 2 months ago
198
        #endif
199
    }
200

            
Bogdan Timofte authored 2 months ago
201
    // MARK: UISceneSession Lifecycle
202

            
203
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
204
        // Called when a new scene session is being created.
205
        // Use this method to select a configuration to create the new scene with.
206
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
207
    }
208

            
209
    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
210
        // Called when the user discards a scene session.
211
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
212
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
213
    }
Bogdan Timofte authored a month ago
214

            
215
    func applicationWillTerminate(_ application: UIApplication) {
216
        _ = appData.flushChargeInsights()
217
        saveContext()
218
    }
219

            
220
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
221
        #if DEBUG
222
        track("Registered for remote notifications with device token length \(deviceToken.count)")
223
        #endif
224
    }
225

            
226
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
227
        track("Remote notification registration failed: \(error.localizedDescription)")
228
    }
229

            
230
    func application(
231
        _ application: UIApplication,
232
        didReceiveRemoteNotification userInfo: [AnyHashable : Any],
233
        fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
234
    ) {
235
        #if DEBUG
236
        track("Received remote notification with keys: \(userInfo.keys.map(String.init(describing:)).joined(separator: ", "))")
237
        #endif
238
        completionHandler(.newData)
239
    }
240

            
241
    lazy var persistentContainer: NSPersistentCloudKitContainer = {
242
        let container = NSPersistentCloudKitContainer(name: "CKModel")
243

            
244
        if let description = container.persistentStoreDescriptions.first {
245
            description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
246
                containerIdentifier: cloudKitContainerIdentifier
247
            )
248
            description.shouldMigrateStoreAutomatically = true
249
            description.shouldInferMappingModelAutomatically = true
250
            description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
251
            description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
252
        }
253

            
254
        container.loadPersistentStores { storeDescription, error in
255
            if let error = error as NSError? {
Bogdan Timofte authored a month ago
256
                // Log the error but do NOT destroy the store — wiping local data and
257
                // waiting for a full CloudKit re-sync is far worse than a degraded launch.
258
                NSLog(
259
                    "Core Data store load failed (url=%@): %@ — %@",
260
                    storeDescription.url?.path ?? "unknown",
261
                    error.localizedDescription,
262
                    error.userInfo
263
                )
Bogdan Timofte authored a month ago
264
                #if DEBUG
265
                fatalError("Unresolved Core Data error \(error), \(error.userInfo)")
266
                #endif
267
            }
268
        }
269

            
270
        container.viewContext.automaticallyMergesChangesFromParent = true
Bogdan Timofte authored a month ago
271
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
Bogdan Timofte authored a month ago
272
        return container
273
    }()
274

            
275
    func saveContext() {
276
        let context = persistentContainer.viewContext
277
        guard context.hasChanges else { return }
278

            
279
        do {
280
            try context.save()
281
        } catch {
282
            let nsError = error as NSError
283
            NSLog("Core Data save failed: %@", nsError.localizedDescription)
284
            #if DEBUG
285
            fatalError("Unresolved Core Data save error \(nsError), \(nsError.userInfo)")
286
            #endif
287
        }
288
    }
289

            
290
    func userNotificationCenter(
291
        _ center: UNUserNotificationCenter,
292
        willPresent notification: UNNotification,
293
        withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
294
    ) {
295
        completionHandler([.banner, .sound, .list])
296
    }
Bogdan Timofte authored 2 months ago
297
}