Files
attendi/Apps/Attendi/Sources/Services/ActivityReporting.swift
T

173 lines
5.7 KiB
Swift

#if os(iOS)
@preconcurrency import ActivityKit
#endif
import Commanding
import Foundation
import Recording
/// The reporting service that surfaces the recording flow in a Live Activity on the Lock Screen and in the Dynamic Island.
///
/// The service implements the `Recording` feature's `Reporting` port over the Live Activity vocabulary shared with the widget
/// extension through the `Commanding` target: it starts a Live Activity when a recording starts, updates its content on every pause,
/// resumption, and processing transition, and ends it — dismissing it right away — when the flow ends. The activity is an auxiliary
/// surface of the recording flow, never a required one: a start that fails, or that the user disallowed in the system settings, is
/// silently ignored, and the platforms without Live Activities reduce the whole service to a no-op.
@MainActor
final class ActivityReporting: Reporting {
// MARK: Properties
#if os(iOS)
/// The Live Activity reflecting the ongoing recording flow, or `nil` while none is.
private var activity: Activity<RecordingActivityAttributes>?
#endif
// MARK: Initializers
/// Creates a reporting service with no Live Activity started yet.
init() {}
// MARK: Methods
/// Starts a Live Activity for a new recording, unless the user disallowed Live Activities or the system refuses to start one.
///
/// - Parameter anchor: The instant the elapsed recording time counts from.
func started(
at anchor: Date
) async {
#if os(iOS)
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
return
}
activity = try? Activity.request(
attributes: RecordingActivityAttributes(),
content: .init(
state: .init(
state: .recording,
anchor: anchor,
elapsed: .zero
),
staleDate: Date.now.addingTimeInterval(Constant.Time.stale)
)
)
#endif
}
/// Updates the Live Activity with the pause of the ongoing recording, freezing its timer at the elapsed recording time.
///
/// - Parameter elapsed: The number of seconds spent recording so far, excluding any time spent paused.
func paused(
elapsed: TimeInterval
) async {
#if os(iOS)
await update(
state: .paused,
anchor: nil,
elapsed: elapsed
)
#endif
}
/// Updates the Live Activity with the resumption of a paused recording, restarting its timer from the given anchor.
///
/// - Parameter anchor: The instant the elapsed recording time counts from, moved back by the time already spent recording.
func resumed(
anchor: Date
) async {
#if os(iOS)
await update(
state: .recording,
anchor: anchor,
elapsed: .zero
)
#endif
}
/// Updates the Live Activity with the processing of the recorded input, freezing its timer at the elapsed recording time.
///
/// - Parameter elapsed: The number of seconds spent recording, excluding any time spent paused.
func processing(
elapsed: TimeInterval
) async {
#if os(iOS)
await update(
state: .processing,
anchor: nil,
elapsed: elapsed
)
#endif
}
/// Ends the Live Activity of the recording flow, dismissing it right away.
func ended() async {
#if os(iOS)
await activity?.end(
activity?.content,
dismissalPolicy: .immediate
)
activity = nil
#endif
}
/// Ends every recording Live Activity still around — including one left over from an earlier run of the app the system killed before
/// it could end its own — dismissing them right away, so an abandoned activity never outlives the process that started it. Meant to
/// run once at launch, before any recording starts, since a fresh launch has no recording an activity could still belong to.
func reset() async {
#if os(iOS)
for activity in Activity<RecordingActivityAttributes>.activities {
await activity.end(nil, dismissalPolicy: .immediate)
}
activity = nil
#endif
}
}
#if os(iOS)
// MARK: - Helpers
private extension ActivityReporting {
/// Updates the dynamic content of the Live Activity, when one is running.
///
/// - Parameters:
/// - state: The state of the recording flow to show.
/// - anchor: The instant the elapsed recording time counts from, or `nil` while the recording is not running.
/// - elapsed: The number of seconds spent recording up to the transition, excluding any time spent paused.
func update(
state: RecordingActivityState,
anchor: Date?,
elapsed: TimeInterval
) async {
await activity?.update(
.init(
state: .init(
state: state,
anchor: anchor,
elapsed: elapsed
),
staleDate: Date.now.addingTimeInterval(Constant.Time.stale)
)
)
}
}
// MARK: - Constants
/// The constant values used across the reporting service.
private enum Constant {
/// The time constants.
enum Time {
/// The span past which the system marks the recording Live Activity stale once the app stops updating it — because it was
/// killed, for example — so an abandoned activity stops looking live instead of lingering for the activity's full lifetime.
/// Generous enough to outlast a recording of any realistic length.
static let stale: TimeInterval = 60 * 60
}
}
#endif