| 1 | #if os(macOS) |
| 2 | import Foundation |
| 3 | |
| 4 | /// How an app's updater behaves between the checks a person asks for. |
| 5 | /// |
| 6 | /// `standard` is Sparkle's own flow: it asks before checking automatically and |
| 7 | /// shows its update alert when a scheduled check finds something. `quiet` never |
| 8 | /// opens a window for a scheduled check. It downloads in the background, installs |
| 9 | /// when the app quits, and publishes `NativeUpdater.pendingUpdate` so the app can |
| 10 | /// show a small notice. While the app reports itself busy (`NativeUpdater.isBusy`, |
| 11 | /// a call, say) scheduled checks wait until it is idle again, so a download never |
| 12 | /// competes with the busy work. A check the person asks for always behaves normally. |
| 13 | /// |
| 14 | /// Quiet also needs the bundle packaged with `"install": "automatic"` in its |
| 15 | /// `mac-updates.json`: that is what turns on automatic checks and downloads |
| 16 | /// without Sparkle's first-launch question. See docs/mac-updates.md. |
| 17 | public enum NativeUpdatePolicy: Sendable, Equatable { |
| 18 | case standard |
| 19 | case quiet |
| 20 | } |
| 21 | |
| 22 | /// What a quiet updater has found on a schedule and not yet installed. |
| 23 | public enum NativePendingUpdate: Sendable, Equatable { |
| 24 | case none |
| 25 | /// Found but not downloaded, because the person turned automatic updates off |
| 26 | /// or the update needs their attention first. Check for Updates shows it. |
| 27 | case available(version: String) |
| 28 | /// Downloaded and staged. It installs when the app quits, or at once through |
| 29 | /// `NativeUpdater.installPendingUpdate()`. |
| 30 | case ready(version: String) |
| 31 | } |
| 32 | |
| 33 | /// Which kind of check Sparkle is about to start, without importing Sparkle. |
| 34 | enum NativeUpdateCheck: Sendable { |
| 35 | case userInitiated, background, information |
| 36 | } |
| 37 | |
| 38 | /// The quiet policy's decisions, kept apart from Sparkle so that they can be tested |
| 39 | /// without a feed, a signed bundle or a network. |
| 40 | struct QuietUpdateRules: Sendable, Equatable { |
| 41 | private(set) var pending: NativePendingUpdate = .none |
| 42 | private(set) var busy = false |
| 43 | /// A scheduled check was declined while busy. Sparkle records the declined check |
| 44 | /// as done and waits a full interval, so without this the update would wait a |
| 45 | /// day for a call that lasted ten minutes. |
| 46 | private(set) var deferredCheck = false |
| 47 | |
| 48 | /// Only scheduled checks wait. A person who chooses Check for Updates mid-call |
| 49 | /// asked for it. |
| 50 | mutating func mayPerform(_ check: NativeUpdateCheck) -> Bool { |
| 51 | guard check == .background, busy else { return true } |
| 52 | deferredCheck = true |
| 53 | return false |
| 54 | } |
| 55 | |
| 56 | /// Returns true when a check deferred while busy should run now. |
| 57 | mutating func setBusy(_ newValue: Bool) -> Bool { |
| 58 | busy = newValue |
| 59 | guard !newValue, deferredCheck else { return false } |
| 60 | deferredCheck = false |
| 61 | return true |
| 62 | } |
| 63 | |
| 64 | mutating func staged(version: String) { pending = .ready(version: version) } |
| 65 | |
| 66 | /// A scheduled update Sparkle would have shown in a window. One already |
| 67 | /// downloaded is ready; otherwise the notice offers it. |
| 68 | mutating func found(version: String, downloaded: Bool) { |
| 69 | pending = downloaded ? .ready(version: version) : .available(version: version) |
| 70 | } |
| 71 | |
| 72 | /// Sparkle's own window has the update now, or the session is over. |
| 73 | mutating func clear() { pending = .none } |
| 74 | |
| 75 | /// Acting on a pending update either relaunches the app or opens Sparkle's |
| 76 | /// window, so neither happens while busy. |
| 77 | var canInstall: Bool { pending != .none && !busy } |
| 78 | } |
| 79 | #endif |
| 80 | |