11import UIKit
22import Foundation
33import BackgroundTasks
4+ import Network
45
56final class BackgroundTaskRefreshDispatcher {
67
@@ -60,6 +61,7 @@ final class BackgroundTaskRefreshDispatcher {
6061 // Launch all refresh tasks in parallel.
6162 let refreshTasks = Task {
6263 do {
64+ async let systemInfo = BackgroundTaskSystemInfo ( )
6365
6466 let startTime = Date . now
6567
@@ -78,7 +80,27 @@ final class BackgroundTaskRefreshDispatcher {
7880 }
7981
8082 let timeTaken = round ( Date . now. timeIntervalSince ( startTime) )
81- ServiceLocator . analytics. track ( event: . BackgroundUpdates. dataSynced ( timeTaken: timeTaken) )
83+
84+ var timeSinceLastRun : TimeInterval ? = nil
85+ if let lastRunTime = UserDefaults . standard [ . lastBackgroundRefreshCompletionTime] as? Date {
86+ timeSinceLastRun = round ( lastRunTime. timeIntervalSinceNow. magnitude)
87+ }
88+
89+ await ServiceLocator . analytics. track ( event: . BackgroundUpdates. dataSynced (
90+ timeTaken: timeTaken,
91+ backgroundTimeGranted: systemInfo. backgroundTimeGranted,
92+ networkType: systemInfo. networkType,
93+ isExpensiveConnection: systemInfo. isExpensiveConnection,
94+ isLowDataMode: systemInfo. isLowDataMode,
95+ isPowered: systemInfo. isPowered,
96+ batteryLevel: systemInfo. batteryLevel,
97+ isLowPowerMode: systemInfo. isLowPowerMode,
98+ timeSinceLastRun: timeSinceLastRun
99+ ) )
100+
101+ // Save date, for use in analytics next time we refresh
102+ UserDefaults . standard [ . lastBackgroundRefreshCompletionTime] = Date . now
103+
82104 backgroundTask. setTaskCompleted ( success: true )
83105
84106 } catch {
@@ -93,7 +115,7 @@ final class BackgroundTaskRefreshDispatcher {
93115 ServiceLocator . analytics. track ( event: . BackgroundUpdates. dataSyncError ( BackgroundError . expired) )
94116 refreshTasks. cancel ( )
95117 }
96- }
118+ }
97119}
98120
99121private extension BackgroundTaskRefreshDispatcher {
@@ -109,3 +131,87 @@ extension BackgroundTaskRefreshDispatcher {
109131 case expired
110132 }
111133}
134+
135+ // MARK: - System Information Helper
136+
137+ private struct NetworkInfo {
138+ let type : String
139+ let isExpensive : Bool
140+ let isLowDataMode : Bool
141+ }
142+
143+ private struct BackgroundTaskSystemInfo {
144+ let backgroundTimeGranted : TimeInterval ?
145+ private let networkInfo : NetworkInfo
146+ let isPowered : Bool
147+ let batteryLevel : Float
148+ let isLowPowerMode : Bool
149+
150+ // Computed properties for clean external access
151+ var networkType : String { networkInfo. type }
152+ var isExpensiveConnection : Bool { networkInfo. isExpensive }
153+ var isLowDataMode : Bool { networkInfo. isLowDataMode }
154+
155+ @MainActor
156+ init ( ) async {
157+ // Background time granted (nil if foreground/unlimited)
158+ let backgroundTime = UIApplication . shared. backgroundTimeRemaining
159+ self . backgroundTimeGranted = backgroundTime < Double . greatestFiniteMagnitude ? backgroundTime : nil
160+
161+ // Network info
162+ self . networkInfo = await Self . getNetworkInfo ( )
163+
164+ // Power and battery info
165+ let device = UIDevice . current
166+ device. isBatteryMonitoringEnabled = true
167+
168+ self . isPowered = device. batteryState == . charging || device. batteryState == . full
169+ self . batteryLevel = device. batteryLevel
170+ self . isLowPowerMode = ProcessInfo . processInfo. isLowPowerModeEnabled
171+
172+ device. isBatteryMonitoringEnabled = false
173+ }
174+
175+ private static func getNetworkInfo( ) async -> NetworkInfo {
176+ return await withCheckedContinuation { continuation in
177+ let monitor = NWPathMonitor ( )
178+
179+ monitor. pathUpdateHandler = { path in
180+ continuation. resume ( returning: NetworkInfo ( path: path) )
181+ monitor. cancel ( )
182+ }
183+
184+ let queue = DispatchQueue ( label: " network.monitor.queue " )
185+ monitor. start ( queue: queue)
186+ }
187+ }
188+ }
189+
190+ private extension NetworkInfo {
191+ init ( path: NWPath ) {
192+ guard path. status == . satisfied else {
193+ self . type = " no_connection "
194+ self . isExpensive = false
195+ self . isLowDataMode = false
196+ return
197+ }
198+
199+ self . type = Self . networkType ( from: path)
200+ self . isExpensive = path. isExpensive
201+ self . isLowDataMode = path. isConstrained
202+ }
203+
204+ private static func networkType( from path: NWPath ) -> String {
205+ if path. usesInterfaceType ( . wifi) {
206+ return " wifi "
207+ } else if path. usesInterfaceType ( . cellular) {
208+ return " cellular "
209+ } else if path. usesInterfaceType ( . wiredEthernet) {
210+ return " ethernet "
211+ } else if path. usesInterfaceType ( . loopback) {
212+ return " loopback "
213+ } else {
214+ return " other "
215+ }
216+ }
217+ }
0 commit comments