// // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import SignalCoreKit import SignalServiceKit public protocol BadgeObserver { func didUpdateBadgeCount( _ badgeManager: BadgeManager, badgeCount: BadgeCount ) } public class BadgeManager { public typealias FetchBadgeCountBlock = () -> BadgeCount private let mainScheduler: Scheduler private let serialScheduler: Scheduler private let fetchBadgeCountBlock: FetchBadgeCountBlock public init( mainScheduler: Scheduler, serialScheduler: Scheduler, fetchBadgeCountBlock: @escaping FetchBadgeCountBlock ) { self.mainScheduler = mainScheduler self.serialScheduler = serialScheduler self.fetchBadgeCountBlock = fetchBadgeCountBlock } public convenience init( databaseStorage: SDSDatabaseStorage, mainScheduler: Scheduler, serialScheduler: Scheduler ) { self.init( mainScheduler: mainScheduler, serialScheduler: serialScheduler, fetchBadgeCountBlock: { return databaseStorage.read { tx -> BadgeCount in return DependenciesBridge.shared.badgeCountFetcher .fetchBadgeCount(tx: tx.asV2Read) } } ) } private var observers = [Weak]() private var shouldFetch: Bool = true private var isFetching: Bool = false private(set) var mostRecentBadgeCount: BadgeCount? private func fetchBadgeValueIfNeeded() { guard shouldFetch, !observers.isEmpty, !isFetching else { return } isFetching = true shouldFetch = false let backgroundTask = OWSBackgroundTask(label: #function) serialScheduler.async { let badgeCount = self.fetchBadgeCountBlock() self.mainScheduler.async { self.isFetching = false self.observers.removeAll(where: { $0.value == nil }) if self.observers.isEmpty { // If there are no observers, we're going to stop fetching badge values for // a while, so don't keep around a value that's potentially outdated. self.mostRecentBadgeCount = nil } else { self.mostRecentBadgeCount = badgeCount self.observers.forEach { $0.value?.didUpdateBadgeCount(self, badgeCount: badgeCount) } self.fetchBadgeValueIfNeeded() } backgroundTask.end() } } } /// Invalidates any previous/active badge count fetches. /// /// Once you call this method, all observers will eventually be notified /// with a new badge value that was computed after this method was called. public func invalidateBadgeValue() { AssertIsOnMainThread() shouldFetch = true fetchBadgeValueIfNeeded() } /// Adds an observer that should be informed when the badge value changes. /// /// If there is already some other observer, and if we already fetched the /// badge value for that observer, we'll provide the most recent cached /// value to the new observer, even if it's slightly out of date. public func addObserver(_ observer: BadgeObserver) { AssertIsOnMainThread() if let mostRecentBadgeCount { observer.didUpdateBadgeCount(self, badgeCount: mostRecentBadgeCount) } observers.append(Weak(value: observer)) fetchBadgeValueIfNeeded() } public func startObservingChanges(in databaseStorage: SDSDatabaseStorage) { databaseStorage.appendDatabaseChangeDelegate(self) } } extension BadgeManager: DatabaseChangeDelegate { public func databaseChangesDidUpdate(databaseChanges: DatabaseChanges) { let badgeMightBeDifferent = ( databaseChanges.didUpdateInteractions || databaseChanges.didUpdateModel(collection: String(describing: ThreadAssociatedData.self)) || databaseChanges.didUpdateModel(collection: String(describing: CallRecord.self)) ) guard badgeMightBeDifferent else { return } invalidateBadgeValue() } public func databaseChangesDidUpdateExternally() { invalidateBadgeValue() } public func databaseChangesDidReset() { invalidateBadgeValue() } }