import Foundation import PromiseKit #if os(iOS) import UIKit #endif public enum BackgroundTask: String { case backgroundFetch = "background-fetch" case lifecycleManagerDidFinishLaunching = "lifecycle-manager-didFinishLaunching" case lifecycleManagerDidEnterBackground = "lifecycle-manager-didEnterBackground" case lifecycleManagerDidBecomeActive = "lifecycle-manager-didBecomeActive" case shortcutItem = "shortcut-item" case handlePushAction = "handle-push-action" case notificationManagerDidReceiveRegistrationToken = "notificationManager-didReceiveRegistrationToken" case zoneManagerPerformEvent = "zone-manager-perform-event" case watchPushAction = "watch-push-action" case webhookSendEphemeral = "webhook-send-ephemeral" case webhookSend = "webhook-send" case webhookInvoke = "webhook-invoke" case manualLocationUpdate = "manual-location-update" case signaledUpdateSensors = "signaled-update-sensors" case connectApi = "connect-api" case realmWrite = "realm-write" case pushLocationRequest = "push-location-request" } public enum BackgroundTaskError: Error { case outOfTime } public protocol HomeAssistantBackgroundTaskRunner { func callAsFunction( withName name: String, wrapping: (TimeInterval?) -> Promise ) -> Promise } // enum for namespacing public enum HomeAssistantBackgroundTask { public static func execute( withName name: String, beginBackgroundTask: (String, @escaping () -> Void) -> (IdentifierType?, TimeInterval?), endBackgroundTask: @escaping (IdentifierType) -> Void, wrapping: (TimeInterval?) -> Promise ) -> Promise { func describe(_ identifier: IdentifierType?) -> String { if let identifier { #if os(iOS) if let identifier = identifier as? UIBackgroundTaskIdentifier { return String(describing: identifier.rawValue) } else { return String(describing: identifier) } #else return String(describing: identifier) #endif } else { return "(none)" } } var identifier: IdentifierType? var remaining: TimeInterval? // we can't guarantee to Swift that this will be assigned, but it will var finished: () -> Void = {} let promise = Promise { seal in (identifier, remaining) = beginBackgroundTask(name) { Current.Log.error("out of time for background task \(name) \(describe(identifier))") seal.reject(BackgroundTaskError.outOfTime) } finished = { seal.fulfill(()) } }.tap { result in guard let endableIdentifier = identifier else { return } let endBackgroundTask = { endBackgroundTask(endableIdentifier) identifier = nil } if case .rejected(BackgroundTaskError.outOfTime) = result { // immediately execute, or we'll be terminated by the system! endBackgroundTask() } else { // give it a run loop, since we want the promise's e.g. completion handlers to be invoked first DispatchQueue.main.async { endBackgroundTask() } } } // make sure we only invoke the promise-returning block once, in case it has side-effects let underlying = wrapping(remaining) let underlyingWithFinished = underlying .ensure { finished() } return firstly { when(fulfilled: [promise.asVoid(), underlyingWithFinished.asVoid()]) }.then { underlying } } }