iOS/Sources/Shared/Notifications/LocalPush/LocalPushManager.swift

202 lines
6.4 KiB
Swift

import HAKit
import PromiseKit
import UserNotifications
public protocol LocalPushManagerDelegate: AnyObject {
func localPushManager(
_ manager: LocalPushManager,
didReceiveRemoteNotification userInfo: [AnyHashable: Any]
)
}
public class LocalPushManager {
public let server: Server
public weak var delegate: LocalPushManagerDelegate?
public static let stateDidChange: Notification.Name = .init(rawValue: "LocalPushManagerStateDidChange")
public enum State: Equatable, Codable {
case establishing
case unavailable
case available(received: Int)
mutating func increment(by count: Int = 1) {
switch self {
case .establishing, .unavailable:
self = .available(received: count)
case let .available(received: originalCount):
self = .available(received: originalCount + count)
}
}
private enum PrimitiveState: String, Codable {
case establishing
case unavailable
case available
}
private var primitiveState: PrimitiveState {
switch self {
case .establishing: return .establishing
case .unavailable: return .unavailable
case .available: return .available
}
}
private var primitiveCount: Int? {
switch self {
case let .available(received: count): return count
case .unavailable, .establishing: return nil
}
}
private enum CodingKeys: CodingKey {
case primitive
case count
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(primitiveState, forKey: .primitive)
try container.encode(primitiveCount, forKey: .count)
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let primitiveState = try container.decode(PrimitiveState.self, forKey: .primitive)
let primitiveCount = try container.decode(Int?.self, forKey: .count)
switch primitiveState {
case .establishing: self = .establishing
case .unavailable: self = .unavailable
case .available: self = .available(received: primitiveCount ?? 0)
}
}
}
public var state: State = .establishing {
didSet {
NotificationCenter.default.post(name: Self.stateDidChange, object: self)
}
}
private var tokens = [HACancellable]()
public init(server: Server) {
self.server = server
updateSubscription()
tokens.append(server.observe { [weak self] _ in
self?.updateSubscription()
})
}
deinit {
invalidate()
tokens.forEach { $0.cancel() }
}
public func invalidate() {
if let subscription {
Current.Log.info("cancelling")
subscription.cancel()
} else {
Current.Log.info("no active subscription")
}
NotificationCenter.default.removeObserver(self)
}
var add: (UNNotificationRequest) -> Promise<Void> = { request in
Promise<Void> { seal in
UNUserNotificationCenter.current().add(request, withCompletionHandler: seal.resolve)
}
}
struct SubscriptionInstance {
let token: HACancellable
let webhookID: String
func cancel() {
Current.Log.info("cancelling subscription")
token.cancel()
}
}
private var subscription: SubscriptionInstance?
private func updateSubscription() {
let webhookID = server.info.connection.webhookID
guard webhookID != subscription?.webhookID else {
// webhookID hasn't changed, so we don't need to reset
return
}
subscription?.cancel()
guard let connection = Current.api(for: server)?.connection else {
Current.Log.error("No API available to update subscription")
return
}
subscription = .init(
token: connection.subscribe(
to: .localPush(webhookID: webhookID, serverVersion: server.info.version),
initiated: { [weak self] result in
self?.handle(initiated: result.map { _ in () })
}, handler: { [weak self] _, value in
self?.handle(event: value)
}
),
webhookID: webhookID
)
}
private func handle(initiated result: Swift.Result<Void, HAError>) {
switch result {
case let .failure(error):
Current.Log.error("failed to subscribe to notifications: \(error)")
state = .unavailable
case .success:
Current.Log.info("started")
state = .available(received: 0)
}
}
private func handle(event: LocalPushEvent) {
Current.Log.debug("handling \(event)")
state.increment()
let baseContent = event.content(server: server)
delegate?.localPushManager(self, didReceiveRemoteNotification: baseContent.userInfo)
guard let api = Current.api(for: server), let connection = api.connection else {
Current.Log.error("No API available to handle local push event")
return
}
firstly {
Current.notificationAttachmentManager.content(from: baseContent, api: api)
}.recover { error in
Current.Log.error("failed to get content, giving default: \(error)")
return .value(baseContent)
}.then { [add] content -> Promise<Void> in
add(UNNotificationRequest(identifier: event.identifier, content: content, trigger: nil))
}.then { [subscription] () -> Promise<Void> in
if let confirmID = event.confirmID, let webhookID = subscription?.webhookID {
return connection.send(.localPushConfirm(
webhookID: webhookID,
confirmID: confirmID
)).promise.map { _ in () }
} else {
return .value(())
}
}.done {
Current.Log.info("added local notification")
}.catch { error in
Current.Log.error("failed to add local notification: \(error)")
}
}
}