Skip to content

Commit

Permalink
Modified background updates.
Browse files Browse the repository at this point in the history
  • Loading branch information
dhermanns committed Dec 20, 2023
1 parent 85e0314 commit 592b229
Show file tree
Hide file tree
Showing 12 changed files with 36 additions and 547 deletions.
300 changes: 0 additions & 300 deletions nightguard WatchKit App/ExtensionDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ class ExtensionDelegate: NSObject, WKApplicationDelegate {
ExtensionDelegate.singleton = self

BackgroundRefreshLogger.info("Application did finish launching")
scheduleBackgroundRefresh()
AppMessageService.singleton.keepAwakePhoneApp()
}

Expand All @@ -67,11 +66,6 @@ class ExtensionDelegate: NSObject, WKApplicationDelegate {
MainController.mainViewModel.refreshData(forceRefresh: true, moveToLatestValue: false)
}

// nightscout data message
WatchMessageService.singleton.onMessage { [weak self] (message: NightscoutDataMessage) in
self?.onNightscoutDataReceivedFromPhoneApp(message.nightscoutData)
}

// user defaults sync message
WatchMessageService.singleton.onMessage { (message: UserDefaultSyncMessage) in

Expand Down Expand Up @@ -127,300 +121,6 @@ class ExtensionDelegate: NSObject, WKApplicationDelegate {
AppState.isUIActive = false

print("Application will resign active.")
scheduleBackgroundRefresh()
AppMessageService.singleton.keepAwakePhoneApp()
}

public func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {

for task in backgroundTasks {

// crash solving trick: acces the task user info to avoid a rare, but weird crash.. (https://forums.developer.apple.com/thread/96504 and https://stackoverflow.com/questions/46464660/wkrefreshbackgroundtask-cleanupstorage-error-attempting-to-reach-file)
userInfoAccess = task.userInfo

if let watchConnectivityBackgroundTask = task as? WKWatchConnectivityRefreshBackgroundTask {
handleWatchConnectivityBackgroundTask(watchConnectivityBackgroundTask)
} else if let snapshotTask = task as? WKSnapshotRefreshBackgroundTask {
handleSnapshotTask(snapshotTask)
} else if let sessionTask = task as? WKURLSessionRefreshBackgroundTask {
handleURLSessionTask(sessionTask)
} else if let refreshTask = task as? WKApplicationRefreshBackgroundTask, WKApplication.shared().applicationState == .background {
handleRefreshTask(refreshTask)
} else {
// not handled!
task.setTaskCompletedWithSnapshot(false)
}
}
}
}

extension ExtensionDelegate {

// MARK:- Background update methods

func handleWatchConnectivityBackgroundTask (_ watchConnectivityBackgroundTask: WKWatchConnectivityRefreshBackgroundTask) {

BackgroundRefreshLogger.info("WKWatchConnectivityRefreshBackgroundTask received")
watchConnectivityBackgroundTask.setTaskCompletedWithSnapshot(false)
}

func handleSnapshotTask(_ snapshotTask : WKSnapshotRefreshBackgroundTask) {

BackgroundRefreshLogger.info("WKSnapshotRefreshBackgroundTask received")

// update user interface with current nightscout data (or error)
let currentNightscoutData = NightscoutCacheService.singleton.getCurrentNightscoutData()
MainController.mainViewModel.pushBackgroundData(newNightscoutData: currentNightscoutData)

snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
}

func handleRefreshTask(_ task : WKRefreshBackgroundTask) {

BackgroundRefreshLogger.info("WKApplicationRefreshBackgroundTask received")
BackgroundRefreshLogger.backgroundRefreshes += 1

scheduleURLSessionIfNeeded()

// schedule the next background refresh
BackgroundRefreshScheduler.instance.schedule()

// Ask to refresh all complications
WidgetCenter.shared.reloadAllTimelines()

task.setTaskCompletedWithSnapshot(false)
}

func handleURLSessionTask(_ sessionTask: WKURLSessionRefreshBackgroundTask) {

BackgroundRefreshLogger.info("WKURLSessionRefreshBackgroundTask received")

let backgroundConfigObject = URLSessionConfiguration.background(withIdentifier: sessionTask.sessionIdentifier)
let backgroundSession = URLSession(configuration: backgroundConfigObject, delegate: self, delegateQueue: nil)
print("Rejoining session ", backgroundSession)

// keep the session background task, it will be ended later... (https://stackoverflow.com/questions/41156386/wkurlsessionrefreshbackgroundtask-isnt-called-when-attempting-to-do-background)
self.pendingBackgroundURLTask = sessionTask
}

@discardableResult
func onNightscoutDataReceivedFromPhoneApp(_ nightscoutData: NightscoutData) -> Bool {

BackgroundRefreshLogger.phoneUpdates += 1

guard !nightscoutData.isOlderThanXMinutes(60) else {
BackgroundRefreshLogger.info("📱Rejected nightscout data (>1hr old!)")
return false
}

let updateResult = updateNightscoutData(nightscoutData, updateComplication: true) // always update complication!
BackgroundRefreshLogger.nightscoutDataReceived(nightscoutData, updateResult: updateResult, updateSource: .phoneApp)
switch updateResult {
case .updateDataIsOld:
BackgroundRefreshLogger.phoneUpdatesWithOldData += 1
case .updateDataAlreadyExists:
BackgroundRefreshLogger.phoneUpdatesWithSameData += 1
case .updated:
BackgroundRefreshLogger.phoneUpdatesWithNewData += 1
}

return updateResult != .updateDataIsOld
}

// MARK:- Internals

fileprivate func scheduleBackgroundRefresh() {
BackgroundRefreshScheduler.instance.schedule()
}

enum UpdateSource {

// the update was initiated by phone app
case phoneApp

// the update was initiated by watch (background URL session)
case urlSession
}

enum UpdateResult {

// update succeeded
case updated

// update data already exists (is the current nightscout data) - no need to update!
case updateDataAlreadyExists

// update data is older than current nightscout data
case updateDataIsOld
}

fileprivate func updateNightscoutData(_ newNightscoutData: NightscoutData, updateComplication: Bool = true) -> UpdateResult {

// synchronize the background update to prevent concurrent modifications
objc_sync_enter(self)

defer {
objc_sync_exit(self)
}

// check the data that already exists on the watch... maybe is newer that the received data
let currentNightscoutData = NightscoutCacheService.singleton.getCurrentNightscoutData()
if currentNightscoutData.time.doubleValue > newNightscoutData.time.doubleValue {

// Old data was received from remote (phone app or URL session)! This can happen because:
// 1. if receiving data from phone app: the watch can have newer data than the phone app (phone app background fetch is once in 5 minutes) or because the delivery is not instantaneous and ... and the watch can update its data in between (when the app enters foreground)
// 2. if receiving data from a URL session: the session can complete later, when there are resources available on the watch to execute it... so there is a posibility than the watch app update itself till then
print("Received older nightscout data than current watch nightscout data!")
return .updateDataIsOld

} else if currentNightscoutData.time.doubleValue == newNightscoutData.time.doubleValue {

// already have this data...
return .updateDataAlreadyExists
}

print("Nightscout data was received from remote (phone app or URL session)!")
NightscoutCacheService.singleton.updateCurrentNightscoutData(newNightscoutData: newNightscoutData)
// if #available(watchOSApplicationExtension 3.0, *) {
// scheduleSnapshotRefresh()
// }

return .updated
}

func synced(_ lock: Any, closure: () -> ()) {
objc_sync_enter(lock)
closure()
objc_sync_exit(lock)
}

func scheduleURLSessionIfNeeded() {

// let currentNightscoutData = NightscoutCacheService.singleton.getCurrentNightscoutData()
// guard currentNightscoutData.isOlderThan5Minutes() else {
// BackgroundRefreshLogger.info("Recent nightscout data, skipping URL session!")
// return
// }

if self.backgroundSession != nil {

if let sessionStartTime = self.sessionStartTime, Calendar.current.date(byAdding: .minute, value: BackgroundRefreshSettings.urlSessionTaskTimeout, to: sessionStartTime)! > Date() {

// URL session running.. we'll let it do its work!
BackgroundRefreshLogger.info("URL session already exists, cannot start a new one!")
return
} else {

// timeout reached for URL session, we'll start a new one!
BackgroundRefreshLogger.info("URL session timeout exceeded, finishing current and starting a new one!")
completePendingURLSessionTask()
}
}

guard let (backgroundSession, downloadTask) = scheduleURLSession() else {
BackgroundRefreshLogger.info("URL session cannot be created, probably base uri is not configured!")
return
}

self.sessionStartTime = Date()
self.backgroundSession = backgroundSession
self.downloadTask = downloadTask
BackgroundRefreshLogger.backgroundURLSessions += 1
BackgroundRefreshLogger.info("URL session started")
}
}

extension ExtensionDelegate: URLSessionDownloadDelegate {

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("Background download was finished.")

// reset the session error
self.sessionError = nil

let nightscoutData = NSData(contentsOf: location as URL)

// extract data on main thead
DispatchQueue.main.async { [unowned self] in

if nightscoutData == nil {
return
}

NightscoutService.singleton.extractApiV2PropertiesData(data: nightscoutData! as Data, { [unowned self] result in

switch result {
case .error(let error):
self.sessionError = error

case .data(let newNightscoutData):
self.sessionError = nil

let updateResult = self.updateNightscoutData(newNightscoutData)
BackgroundRefreshLogger.nightscoutDataReceived(newNightscoutData, updateResult: updateResult, updateSource: .urlSession)
switch updateResult {
case .updateDataIsOld:
BackgroundRefreshLogger.backgroundURLSessionUpdatesWithOldData += 1
BackgroundRefreshLogger.info("URL session data: OLD")
case .updateDataAlreadyExists:
BackgroundRefreshLogger.backgroundURLSessionUpdatesWithSameData += 1
BackgroundRefreshLogger.info("URL session data: EXISTING")
case .updated:
BackgroundRefreshLogger.backgroundURLSessionUpdatesWithNewData += 1
BackgroundRefreshLogger.info("URL session data: NEW")
}
}
})
}

completePendingURLSessionTask()
}

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
print("Background url session completed with error: \(String(describing: error))")
if let error = error {
BackgroundRefreshLogger.info("URL session did complete with error: \(error)")
completePendingURLSessionTask()
}

// keep the session error (if any!)
self.sessionError = error
}

func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
BackgroundRefreshLogger.info("URL session did finish events")
// completePendingURLSessionTask()
}

fileprivate func completePendingURLSessionTask() {

self.backgroundSession?.invalidateAndCancel()
self.backgroundSession = nil
self.downloadTask = nil
self.sessionStartTime = nil
(self.pendingBackgroundURLTask as? WKRefreshBackgroundTask)?.setTaskCompletedWithSnapshot(false)
self.pendingBackgroundURLTask = nil

BackgroundRefreshLogger.info("URL session COMPLETED")
}

func scheduleURLSession() -> (URLSession, URLSessionDownloadTask)? {

let baseUri = UserDefaultsRepository.baseUri.value
if baseUri == "" {
return nil
}

let backgroundConfigObject = URLSessionConfiguration.background(withIdentifier: NSUUID().uuidString)
backgroundConfigObject.sessionSendsLaunchEvents = true
// backgroundConfigObject.timeoutIntervalForRequest = 15 // 15 seconds timeout for request (after 15 seconds, the task is finished and a crash occurs, so... we have to stop it somehow!)
// backgroundConfigObject.timeoutIntervalForResource = 15 // the same for retry interval (no retries!)
let backgroundSession = URLSession(configuration: backgroundConfigObject, delegate: self, delegateQueue: nil)

let downloadURL = URL(string: baseUri + "/pebble")!
let downloadTask = backgroundSession.downloadTask(with: downloadURL)
downloadTask.resume()

return (backgroundSession, downloadTask)
}
}
2 changes: 1 addition & 1 deletion nightguard WatchKit App/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>872</string>
<string>874</string>
<key>CLKComplicationPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ComplicationController</string>
<key>NSAppTransportSecurity</key>
Expand Down
35 changes: 1 addition & 34 deletions nightguard WatchKit App/app/BackgroundRefreshLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class BackgroundRefreshLogger {

private static var logStartTime: Date?
private static var appStartTime: Date?
private static let showLogs = BackgroundRefreshSettings.showBackgroundTasksLogs
private static let showLogs = true

static func info(_ text: String) {
resetStatsDataIfNeeded()
Expand All @@ -62,39 +62,6 @@ class BackgroundRefreshLogger {
}
}


static func nightscoutDataReceived(_ nightscoutData: NightscoutData, updateResult: ExtensionDelegate.UpdateResult, updateSource: ExtensionDelegate.UpdateSource) {

var updateSourceString = ""
switch updateSource {
case .phoneApp :
updateSourceString = "📱"
default:
updateSourceString = ""
}

var updateResultString = ""
switch updateResult {
case .updated:
updateResultString = "NEW"
case .updateDataAlreadyExists:
updateResultString = "EXISTING"
case .updateDataIsOld:
updateResultString = "OLD"
}

let nightscoutDataTime = Date(timeIntervalSince1970: nightscoutData.time.doubleValue / 1000)
let nightscoutDataTimeString = formattedTime(nightscoutDataTime, showSeconds: false)

let logEntry = formattedTime(Date()) + " " + updateSourceString + updateResultString + " (\(nightscoutData.sgv)@\(nightscoutDataTimeString))"
NSLog(logEntry)

if showLogs {
receivedData.append(logEntry)
}

}

private static func resetStatsDataIfNeeded() {

let now = Date()
Expand Down
Loading

0 comments on commit 592b229

Please sign in to comment.