USB-Meter / USB Meter / AppDelegate.swift
Newer Older
253 lines | 9.078kb
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 2 months ago
18
enum Constants {
19
    static let chartUnderscan: CGFloat = 0.5
20
    static let chartOverscan: CGFloat = 1 - chartUnderscan
21
}
22
// MARK: Clock
23

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

            
Bogdan Timofte authored a month ago
37
public func restoreTrace(_ message: String) {
38
    restoreLogger.debug("\(message, privacy: .public)")
39
}
40

            
Bogdan Timofte authored 2 months ago
41
private func shouldEmitTrackMessage(_ message: String, file: String, function: String) -> Bool {
42
    #if DEBUG
43
    if ProcessInfo.processInfo.environment["USB_METER_VERBOSE_LOGS"] == "1" {
44
        return true
45
    }
46

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

            
76
    if importantMarkers.contains(where: { message.contains($0) }) {
77
        return true
78
    }
79

            
80
    let noisyFunctions: Set<String> = [
81
        "logRuntimeICloudDiagnostics()",
82
        "refreshCloudAvailability(reason:)",
83
        "start()",
84
        "centralManagerDidUpdateState(_:)",
85
        "discoveredMeter(peripheral:advertising:rssi:)",
86
        "connect()",
87
        "connectionEstablished()",
88
        "peripheral(_:didDiscoverServices:)",
89
        "peripheral(_:didDiscoverCharacteristicsFor:error:)",
90
        "refreshOperationalStateIfReady()",
91
        "peripheral(_:didUpdateNotificationStateFor:error:)",
92
        "scheduleDataDumpRequest(after:reason:)"
93
    ]
94

            
95
    if noisyFunctions.contains(function) {
96
        return false
97
    }
98

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

            
116
    if noisyMarkers.contains(where: { message.contains($0) }) {
117
        return false
118
    }
119
    #endif
120

            
121
    return true
122
    #else
123
    _ = file
124
    _ = function
125
    return false
126
    #endif
127
}
128

            
Bogdan Timofte authored 2 months ago
129
@UIApplicationMain
Bogdan Timofte authored a month ago
130
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
131
    private let cloudKitContainerIdentifier = "iCloud.ro.xdev.USB-Meter"
Bogdan Timofte authored 2 months ago
132

            
133

            
134
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Bogdan Timofte authored 2 months ago
135
        logRuntimeICloudDiagnostics()
Bogdan Timofte authored a month ago
136
        UNUserNotificationCenter.current().delegate = self
137
        application.registerForRemoteNotifications()
138
        appData.activateChargeInsights(context: persistentContainer.viewContext)
Bogdan Timofte authored 2 months ago
139
        return true
140
    }
141

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

            
152
            track("CloudKit account status: \(status.rawValue)")
153
        }
Bogdan Timofte authored 2 months ago
154
        #endif
155
    }
156

            
Bogdan Timofte authored 2 months ago
157
    // MARK: UISceneSession Lifecycle
158

            
159
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
160
        // Called when a new scene session is being created.
161
        // Use this method to select a configuration to create the new scene with.
162
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
163
    }
164

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

            
171
    func applicationWillTerminate(_ application: UIApplication) {
172
        _ = appData.flushChargeInsights()
173
        saveContext()
174
    }
175

            
176
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
177
        #if DEBUG
178
        track("Registered for remote notifications with device token length \(deviceToken.count)")
179
        #endif
180
    }
181

            
182
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
183
        track("Remote notification registration failed: \(error.localizedDescription)")
184
    }
185

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

            
197
    lazy var persistentContainer: NSPersistentCloudKitContainer = {
198
        let container = NSPersistentCloudKitContainer(name: "CKModel")
199

            
200
        if let description = container.persistentStoreDescriptions.first {
201
            description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
202
                containerIdentifier: cloudKitContainerIdentifier
203
            )
204
            description.shouldMigrateStoreAutomatically = true
205
            description.shouldInferMappingModelAutomatically = true
206
            description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
207
            description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
208
        }
209

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

            
226
        container.viewContext.automaticallyMergesChangesFromParent = true
Bogdan Timofte authored a month ago
227
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
Bogdan Timofte authored a month ago
228
        return container
229
    }()
230

            
231
    func saveContext() {
232
        let context = persistentContainer.viewContext
233
        guard context.hasChanges else { return }
234

            
235
        do {
236
            try context.save()
237
        } catch {
238
            let nsError = error as NSError
239
            NSLog("Core Data save failed: %@", nsError.localizedDescription)
240
            #if DEBUG
241
            fatalError("Unresolved Core Data save error \(nsError), \(nsError.userInfo)")
242
            #endif
243
        }
244
    }
245

            
246
    func userNotificationCenter(
247
        _ center: UNUserNotificationCenter,
248
        willPresent notification: UNNotification,
249
        withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
250
    ) {
251
        completionHandler([.banner, .sound, .list])
252
    }
Bogdan Timofte authored 2 months ago
253
}