USB-Meter / USB Meter / AppDelegate.swift
Newer Older
262 lines | 9.508kb
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? {
206
                NSLog("Core Data store load failed: %@", error.localizedDescription)
207

            
208
                if let storeURL = storeDescription.url {
209
                    let coordinator = container.persistentStoreCoordinator
210
                    do {
211
                        try coordinator.destroyPersistentStore(
212
                            at: storeURL,
213
                            ofType: storeDescription.type,
214
                            options: nil
215
                        )
216
                        try coordinator.addPersistentStore(
217
                            ofType: storeDescription.type,
218
                            configurationName: nil,
219
                            at: storeURL,
220
                            options: storeDescription.options
221
                        )
222
                        NSLog("Recovered CloudKit store by recreating it at %@", storeURL.path)
223
                        return
224
                    } catch {
225
                        NSLog("Core Data recovery attempt failed: %@", error.localizedDescription)
226
                    }
227
                }
228

            
229
                #if DEBUG
230
                fatalError("Unresolved Core Data error \(error), \(error.userInfo)")
231
                #endif
232
            }
233
        }
234

            
235
        container.viewContext.automaticallyMergesChangesFromParent = true
236
        container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
237
        return container
238
    }()
239

            
240
    func saveContext() {
241
        let context = persistentContainer.viewContext
242
        guard context.hasChanges else { return }
243

            
244
        do {
245
            try context.save()
246
        } catch {
247
            let nsError = error as NSError
248
            NSLog("Core Data save failed: %@", nsError.localizedDescription)
249
            #if DEBUG
250
            fatalError("Unresolved Core Data save error \(nsError), \(nsError.userInfo)")
251
            #endif
252
        }
253
    }
254

            
255
    func userNotificationCenter(
256
        _ center: UNUserNotificationCenter,
257
        willPresent notification: UNNotification,
258
        withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
259
    ) {
260
        completionHandler([.banner, .sound, .list])
261
    }
Bogdan Timofte authored 2 months ago
262
}