| 1 | import Foundation |
| 2 | |
| 3 | /// Keeps the Mac's display awake through a call (2026-10-19, for 0.4.10): |
| 4 | /// a call is often watched rather than typed into, and the display going |
| 5 | /// to sleep under it reads as the call having ended. Held from the moment |
| 6 | /// the call is joined until it is left or ends, by a process activity |
| 7 | /// that disables idle display sleep only. It does not stop a person |
| 8 | /// locking the Mac, or the lid closing, and it goes with the process: the |
| 9 | /// system ends an activity whose process quits or crashes. The iPhone |
| 10 | /// keeps its own screen on with isIdleTimerDisabled. |
| 11 | @MainActor public final class CallWake { |
| 12 | public static let reason = "In a Calls call" |
| 13 | |
| 14 | /// What begins and ends the activity: ProcessInfo's, or a test's. |
| 15 | public protocol Activities { |
| 16 | func begin(reason: String) -> NSObjectProtocol |
| 17 | func end(_ activity: NSObjectProtocol) |
| 18 | } |
| 19 | |
| 20 | struct ProcessActivities: Activities { |
| 21 | func begin(reason: String) -> NSObjectProtocol { |
| 22 | ProcessInfo.processInfo.beginActivity( |
| 23 | options: [.idleDisplaySleepDisabled, .userInitiated], reason: reason) |
| 24 | } |
| 25 | func end(_ activity: NSObjectProtocol) { ProcessInfo.processInfo.endActivity(activity) } |
| 26 | } |
| 27 | |
| 28 | private let activities: Activities |
| 29 | private var activity: NSObjectProtocol? |
| 30 | |
| 31 | /// Whether the display is being kept awake now. |
| 32 | public var holding: Bool { activity != nil } |
| 33 | |
| 34 | public init(activities: Activities? = nil) { |
| 35 | self.activities = activities ?? ProcessActivities() |
| 36 | } |
| 37 | |
| 38 | /// In a call or not: begins the activity on the way in, ends it on the |
| 39 | /// way out, and does nothing when nothing changed, so it can be told |
| 40 | /// as often as the state is published. |
| 41 | public func update(inCall: Bool) { |
| 42 | if inCall, activity == nil { |
| 43 | activity = activities.begin(reason: Self.reason) |
| 44 | } else if !inCall, let held = activity { |
| 45 | activities.end(held) |
| 46 | activity = nil |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | /// Whether a call in `phase` (the room's: "admitted", "ended", …) keeps |
| 51 | /// the display awake while `joining`. |
| 52 | public static func inCall(joining: Bool, phase: String?) -> Bool { |
| 53 | joining && phase != "ended" |
| 54 | } |
| 55 | } |
| 56 | |