2026-07-05 22:06:45 +02:00
|
|
|
#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
|
|
|
|
|
),
|
2026-07-06 23:05:24 +02:00
|
|
|
staleDate: Date.now.addingTimeInterval(Constant.Time.stale)
|
2026-07-05 22:06:45 +02:00
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
#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
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 23:05:24 +02:00
|
|
|
/// 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 22:06:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#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
|
|
|
|
|
),
|
2026-07-06 23:05:24 +02:00
|
|
|
staleDate: Date.now.addingTimeInterval(Constant.Time.stale)
|
2026-07-05 22:06:45 +02:00
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
2026-07-06 23:05:24 +02:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-05 22:06:45 +02:00
|
|
|
#endif
|