USB-Meter / USB Meter / AppDelegate.swift
Newer Older
264 lines | 9.628kb
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 a month ago
139
        configureNavigationBarAppearance()
Bogdan Timofte authored 2 months ago
140
        return true
141
    }
142

            
Bogdan Timofte authored a month ago
143
    private func configureNavigationBarAppearance() {
144
        let titleFont = UIFont.systemFont(ofSize: 19, weight: .semibold)
145
        let appearance = UINavigationBarAppearance()
146
        appearance.configureWithDefaultBackground()
147
        appearance.titleTextAttributes = [.font: titleFont]
148
        UINavigationBar.appearance().standardAppearance = appearance
149
        UINavigationBar.appearance().scrollEdgeAppearance = appearance
150
        UINavigationBar.appearance().compactAppearance = appearance
151
    }
152

            
Bogdan Timofte authored 2 months ago
153
    private func logRuntimeICloudDiagnostics() {
154
        #if DEBUG
155
        let hasUbiquityIdentityToken = FileManager.default.ubiquityIdentityToken != nil
156
        track("Runtime iCloud diagnostics: ubiquityIdentityTokenAvailable=\(hasUbiquityIdentityToken)")
Bogdan Timofte authored a month ago
157
        CKContainer(identifier: cloudKitContainerIdentifier).accountStatus { status, error in
158
            if let error {
159
                track("CloudKit account status error: \(error.localizedDescription)")
160
                return
161
            }
162

            
163
            track("CloudKit account status: \(status.rawValue)")
164
        }
Bogdan Timofte authored 2 months ago
165
        #endif
166
    }
167

            
Bogdan Timofte authored 2 months ago
168
    // MARK: UISceneSession Lifecycle
169

            
170
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
171
        // Called when a new scene session is being created.
172
        // Use this method to select a configuration to create the new scene with.
173
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
174
    }
175

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

            
182
    func applicationWillTerminate(_ application: UIApplication) {
183
        _ = appData.flushChargeInsights()
184
        saveContext()
185
    }
186

            
187
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
188
        #if DEBUG
189
        track("Registered for remote notifications with device token length \(deviceToken.count)")
190
        #endif
191
    }
192

            
193
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
194
        track("Remote notification registration failed: \(error.localizedDescription)")
195
    }
196

            
197
    func application(
198
        _ application: UIApplication,
199
        didReceiveRemoteNotification userInfo: [AnyHashable : Any],
200
        fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
201
    ) {
202
        #if DEBUG
203
        track("Received remote notification with keys: \(userInfo.keys.map(String.init(describing:)).joined(separator: ", "))")
204
        #endif
205
        completionHandler(.newData)
206
    }
207

            
208
    lazy var persistentContainer: NSPersistentCloudKitContainer = {
209
        let container = NSPersistentCloudKitContainer(name: "CKModel")
210

            
211
        if let description = container.persistentStoreDescriptions.first {
212
            description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
213
                containerIdentifier: cloudKitContainerIdentifier
214
            )
215
            description.shouldMigrateStoreAutomatically = true
216
            description.shouldInferMappingModelAutomatically = true
217
            description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
218
            description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
219
        }
220

            
221
        container.loadPersistentStores { storeDescription, error in
222
            if let error = error as NSError? {
Bogdan Timofte authored a month ago
223
                // Log the error but do NOT destroy the store — wiping local data and
224
                // waiting for a full CloudKit re-sync is far worse than a degraded launch.
225
                NSLog(
226
                    "Core Data store load failed (url=%@): %@ — %@",
227
                    storeDescription.url?.path ?? "unknown",
228
                    error.localizedDescription,
229
                    error.userInfo
230
                )
Bogdan Timofte authored a month ago
231
                #if DEBUG
232
                fatalError("Unresolved Core Data error \(error), \(error.userInfo)")
233
                #endif
234
            }
235
        }
236

            
237
        container.viewContext.automaticallyMergesChangesFromParent = true
Bogdan Timofte authored a month ago
238
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
Bogdan Timofte authored a month ago
239
        return container
240
    }()
241

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

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

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