USB-Meter / USB Meter / AppDelegate.swift
Newer Older
247 lines | 8.872kb
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 2 months ago
11
import UIKit
Bogdan Timofte authored a month ago
12
import UserNotifications
Bogdan Timofte authored 2 months ago
13

            
14
//let btSerial = BluetoothSerial(delegate: BSD())
15
let appData = AppData()
16
enum Constants {
17
    static let chartUnderscan: CGFloat = 0.5
18
    static let chartOverscan: CGFloat = 1 - chartUnderscan
19
}
20
// MARK: Clock
21

            
22
// MARK: Debug
23
public func track(_ message: String = "", file: String = #file, function: String = #function, line: Int = #line ) {
Bogdan Timofte authored 2 months ago
24
    guard shouldEmitTrackMessage(message, file: file, function: function) else {
25
        return
26
    }
Bogdan Timofte authored 2 months ago
27
    let date = Date()
28
    let calendar = Calendar.current
29
    let hour = calendar.component(.hour, from: date)
30
    let minutes = calendar.component(.minute, from: date)
31
    let seconds = calendar.component(.second, from: date)
32
    print("\(hour):\(minutes):\(seconds) - \(file):\(line) - \(function) \(message)")
33
}
34

            
Bogdan Timofte authored 2 months ago
35
private func shouldEmitTrackMessage(_ message: String, file: String, function: String) -> Bool {
36
    #if DEBUG
37
    if ProcessInfo.processInfo.environment["USB_METER_VERBOSE_LOGS"] == "1" {
38
        return true
39
    }
40

            
41
    #if targetEnvironment(macCatalyst)
42
    let importantMarkers = [
43
        "Error",
44
        "error",
45
        "Failed",
46
        "failed",
47
        "timeout",
48
        "Timeout",
49
        "Missing",
50
        "missing",
51
        "overflow",
52
        "Disconnect",
53
        "disconnect",
54
        "Disconnected",
55
        "unauthorized",
56
        "not authorized",
57
        "not supported",
58
        "Unexpected",
59
        "Invalid Context",
60
        "ignored",
61
        "Guard:",
62
        "Skip data request",
63
        "Dropping unsolicited data",
64
        "This is not possible!",
65
        "Inferred",
66
        "Clearing",
67
        "Reconnecting"
68
    ]
69

            
70
    if importantMarkers.contains(where: { message.contains($0) }) {
71
        return true
72
    }
73

            
74
    let noisyFunctions: Set<String> = [
75
        "logRuntimeICloudDiagnostics()",
76
        "refreshCloudAvailability(reason:)",
77
        "start()",
78
        "centralManagerDidUpdateState(_:)",
79
        "discoveredMeter(peripheral:advertising:rssi:)",
80
        "connect()",
81
        "connectionEstablished()",
82
        "peripheral(_:didDiscoverServices:)",
83
        "peripheral(_:didDiscoverCharacteristicsFor:error:)",
84
        "refreshOperationalStateIfReady()",
85
        "peripheral(_:didUpdateNotificationStateFor:error:)",
86
        "scheduleDataDumpRequest(after:reason:)"
87
    ]
88

            
89
    if noisyFunctions.contains(function) {
90
        return false
91
    }
92

            
93
    let noisyMarkers = [
94
        "Runtime iCloud diagnostics",
95
        "iCloud availability",
96
        "Starting Bluetooth manager",
97
        "Bluetooth is On... Start scanning...",
98
        "adding new USB Meter",
99
        "Connect called for",
100
        "Connection established for",
101
        "Optional([<CBService:",
102
        "Optional([<CBCharacteristic:",
103
        "Waiting for notifications on",
104
        "Notification state updated for",
105
        "Peripheral ready with notify",
106
        "Schedule data request in",
107
        "Operational state changed"
108
    ]
109

            
110
    if noisyMarkers.contains(where: { message.contains($0) }) {
111
        return false
112
    }
113
    #endif
114

            
115
    return true
116
    #else
117
    _ = file
118
    _ = function
119
    return false
120
    #endif
121
}
122

            
Bogdan Timofte authored 2 months ago
123
@UIApplicationMain
Bogdan Timofte authored a month ago
124
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
125
    private let cloudKitContainerIdentifier = "iCloud.ro.xdev.USB-Meter"
Bogdan Timofte authored 2 months ago
126

            
127

            
128
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Bogdan Timofte authored 2 months ago
129
        logRuntimeICloudDiagnostics()
Bogdan Timofte authored a month ago
130
        UNUserNotificationCenter.current().delegate = self
131
        application.registerForRemoteNotifications()
132
        appData.activateChargeInsights(context: persistentContainer.viewContext)
Bogdan Timofte authored 2 months ago
133
        return true
134
    }
135

            
Bogdan Timofte authored 2 months ago
136
    private func logRuntimeICloudDiagnostics() {
137
        #if DEBUG
138
        let hasUbiquityIdentityToken = FileManager.default.ubiquityIdentityToken != nil
139
        track("Runtime iCloud diagnostics: ubiquityIdentityTokenAvailable=\(hasUbiquityIdentityToken)")
Bogdan Timofte authored a month ago
140
        CKContainer(identifier: cloudKitContainerIdentifier).accountStatus { status, error in
141
            if let error {
142
                track("CloudKit account status error: \(error.localizedDescription)")
143
                return
144
            }
145

            
146
            track("CloudKit account status: \(status.rawValue)")
147
        }
Bogdan Timofte authored 2 months ago
148
        #endif
149
    }
150

            
Bogdan Timofte authored 2 months ago
151
    // MARK: UISceneSession Lifecycle
152

            
153
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
154
        // Called when a new scene session is being created.
155
        // Use this method to select a configuration to create the new scene with.
156
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
157
    }
158

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

            
165
    func applicationWillTerminate(_ application: UIApplication) {
166
        _ = appData.flushChargeInsights()
167
        saveContext()
168
    }
169

            
170
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
171
        #if DEBUG
172
        track("Registered for remote notifications with device token length \(deviceToken.count)")
173
        #endif
174
    }
175

            
176
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
177
        track("Remote notification registration failed: \(error.localizedDescription)")
178
    }
179

            
180
    func application(
181
        _ application: UIApplication,
182
        didReceiveRemoteNotification userInfo: [AnyHashable : Any],
183
        fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
184
    ) {
185
        #if DEBUG
186
        track("Received remote notification with keys: \(userInfo.keys.map(String.init(describing:)).joined(separator: ", "))")
187
        #endif
188
        completionHandler(.newData)
189
    }
190

            
191
    lazy var persistentContainer: NSPersistentCloudKitContainer = {
192
        let container = NSPersistentCloudKitContainer(name: "CKModel")
193

            
194
        if let description = container.persistentStoreDescriptions.first {
195
            description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
196
                containerIdentifier: cloudKitContainerIdentifier
197
            )
198
            description.shouldMigrateStoreAutomatically = true
199
            description.shouldInferMappingModelAutomatically = true
200
            description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
201
            description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
202
        }
203

            
204
        container.loadPersistentStores { storeDescription, error in
205
            if let error = error as NSError? {
Bogdan Timofte authored a month ago
206
                // Log the error but do NOT destroy the store — wiping local data and
207
                // waiting for a full CloudKit re-sync is far worse than a degraded launch.
208
                NSLog(
209
                    "Core Data store load failed (url=%@): %@ — %@",
210
                    storeDescription.url?.path ?? "unknown",
211
                    error.localizedDescription,
212
                    error.userInfo
213
                )
Bogdan Timofte authored a month ago
214
                #if DEBUG
215
                fatalError("Unresolved Core Data error \(error), \(error.userInfo)")
216
                #endif
217
            }
218
        }
219

            
220
        container.viewContext.automaticallyMergesChangesFromParent = true
Bogdan Timofte authored a month ago
221
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
Bogdan Timofte authored a month ago
222
        return container
223
    }()
224

            
225
    func saveContext() {
226
        let context = persistentContainer.viewContext
227
        guard context.hasChanges else { return }
228

            
229
        do {
230
            try context.save()
231
        } catch {
232
            let nsError = error as NSError
233
            NSLog("Core Data save failed: %@", nsError.localizedDescription)
234
            #if DEBUG
235
            fatalError("Unresolved Core Data save error \(nsError), \(nsError.userInfo)")
236
            #endif
237
        }
238
    }
239

            
240
    func userNotificationCenter(
241
        _ center: UNUserNotificationCenter,
242
        willPresent notification: UNNotification,
243
        withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
244
    ) {
245
        completionHandler([.banner, .sound, .list])
246
    }
Bogdan Timofte authored 2 months ago
247
}