You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
326 lines
11 KiB
326 lines
11 KiB
//
|
|
// AppEnvironment.swift
|
|
// LeCountdown
|
|
//
|
|
// Created by Laurent Morvillier on 31/01/2023.
|
|
//
|
|
|
|
import Foundation
|
|
import ActivityKit
|
|
import BackgroundTasks
|
|
import SwiftUI
|
|
import Intents
|
|
import AudioToolbox
|
|
|
|
enum BGTaskIdentifier : String {
|
|
case refresh = "com.staxriver.lecountdown.refresh"
|
|
}
|
|
|
|
class Conductor: ObservableObject {
|
|
|
|
static let maestro: Conductor = Conductor()
|
|
|
|
@Published var soundPlayer: SoundPlayer? = nil
|
|
|
|
@UserDefault(PreferenceKey.countdowns.rawValue, defaultValue: [:]) static var savedCountdowns: [String : DateInterval]
|
|
@UserDefault(PreferenceKey.stopwatches.rawValue, defaultValue: [:]) static var savedStopwatches: [String : Date]
|
|
|
|
@Published private (set) var liveTimers: [LiveTimer] = []
|
|
|
|
init() {
|
|
self.currentCountdowns = Conductor.savedCountdowns
|
|
self.currentStopwatches = Conductor.savedStopwatches
|
|
}
|
|
|
|
@Published var cancelledCountdowns: [String] = []
|
|
|
|
@Published var currentCountdowns: [String : DateInterval] = [:] {
|
|
didSet {
|
|
Conductor.savedCountdowns = currentCountdowns
|
|
Logger.log("**** currentCountdowns didSet, count = \(currentCountdowns.count)")
|
|
withAnimation {
|
|
self._buildLiveTimers()
|
|
}
|
|
}
|
|
}
|
|
|
|
@Published var currentStopwatches: [String : Date] = [:] {
|
|
didSet {
|
|
Conductor.savedStopwatches = currentStopwatches
|
|
withAnimation {
|
|
self._buildLiveTimers()
|
|
}
|
|
}
|
|
}
|
|
|
|
func removeLiveTimer(id: String) {
|
|
self.liveTimers.removeAll(where: { $0.id == id })
|
|
self.cancelledCountdowns.removeAll(where: { $0 == id })
|
|
}
|
|
|
|
fileprivate func _buildLiveTimers() {
|
|
|
|
Logger.log("_buildLiveTimers")
|
|
|
|
let liveCountdowns = self.currentCountdowns.map {
|
|
return LiveTimer(id: $0, date: $1.end)
|
|
}
|
|
|
|
// add countdown if not present
|
|
for liveCountdown in liveCountdowns {
|
|
if let index = self.liveTimers.firstIndex(where: { $0.id == liveCountdown.id }) {
|
|
self.liveTimers.remove(at: index)
|
|
self.liveTimers.insert(liveCountdown, at: index)
|
|
} else {
|
|
self.liveTimers.append(liveCountdown)
|
|
}
|
|
}
|
|
|
|
let liveStopwatches = self.currentStopwatches.map {
|
|
return LiveTimer(id: $0, date: $1)
|
|
}
|
|
for liveStopwatch in liveStopwatches {
|
|
if let index = self.liveTimers.firstIndex(where: { $0.id == liveStopwatch.id }) {
|
|
self.liveTimers.remove(at: index)
|
|
self.liveTimers.insert(liveStopwatch, at: index)
|
|
} else {
|
|
self.liveTimers.append(liveStopwatch)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func notifyUser(countdownId: String) {
|
|
self._playSound(timerId: countdownId)
|
|
self._endCountdown(countdownId: countdownId, cancel: false)
|
|
}
|
|
|
|
fileprivate func _recordActivity(countdownId: String) {
|
|
let context = PersistenceController.shared.container.viewContext
|
|
if let countdown = context.object(stringId: countdownId) as? Countdown,
|
|
let dateInterval = self.currentCountdowns[countdownId] {
|
|
do {
|
|
try CoreDataRequests.recordActivity(timer: countdown, dateInterval: dateInterval)
|
|
} catch {
|
|
Logger.error(error)
|
|
// TODO: show error to user
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Countdown
|
|
|
|
func startCountdown(_ date: Date, countdown: Countdown) {
|
|
// DispatchQueue.main.async {
|
|
|
|
Logger.log("Starts countdown: \(countdown.displayName)")
|
|
|
|
// cleanup existing countdowns
|
|
self.removeLiveTimer(id: countdown.stringId)
|
|
|
|
// self._cleanupTimers.removeValue(forKey: countdown.stringId)
|
|
|
|
let dateInterval = DateInterval(start: Date(), end: date)
|
|
self.currentCountdowns[countdown.stringId] = dateInterval
|
|
|
|
// self._launchLiveActivity(countdown: countdown, endDate: date)
|
|
|
|
self._createTimerIntent(countdown)
|
|
|
|
Logger.log("countdowns count = \(self.currentCountdowns.count)")
|
|
|
|
// }
|
|
}
|
|
|
|
func cancelCountdown(id: String) {
|
|
CountdownScheduler.master.cancelCurrentNotifications(countdownId: id)
|
|
self.stopSoundIfPossible()
|
|
self.cancelledCountdowns.append(id)
|
|
self._endCountdown(countdownId: id, cancel: true)
|
|
}
|
|
|
|
fileprivate func _endCountdown(countdownId: String, cancel: Bool) {
|
|
DispatchQueue.main.async {
|
|
if !cancel {
|
|
self._recordActivity(countdownId: countdownId)
|
|
}
|
|
self.currentCountdowns.removeValue(forKey: countdownId)
|
|
}
|
|
}
|
|
|
|
// MARK: - Stopwatch
|
|
|
|
func startStopwatch(_ stopwatch: Stopwatch) {
|
|
DispatchQueue.main.async {
|
|
let now = Date()
|
|
Conductor.maestro.currentStopwatches[stopwatch.stringId] = now
|
|
self._launchLiveActivity(stopwatch: stopwatch, start: now)
|
|
|
|
self._createTimerIntent(stopwatch)
|
|
|
|
}
|
|
}
|
|
|
|
func stopStopwatch(_ stopwatch: Stopwatch, end: Date? = nil) {
|
|
if let start = Conductor.maestro.currentStopwatches[stopwatch.stringId] {
|
|
Conductor.maestro.currentStopwatches.removeValue(forKey: stopwatch.stringId)
|
|
do {
|
|
try CoreDataRequests.recordActivity(timer: stopwatch, dateInterval: DateInterval(start: start, end: end ?? Date()))
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
self._endLiveActivity(timerId: stopwatch.stringId)
|
|
}
|
|
}
|
|
|
|
// MARK: - Cleanup
|
|
|
|
func cleanup() {
|
|
self._cleanupCountdowns()
|
|
self._buildLiveTimers()
|
|
}
|
|
|
|
fileprivate func _cleanupCountdowns() {
|
|
let now = Date()
|
|
for (key, value) in self.currentCountdowns {
|
|
if value.end < now {
|
|
self._endCountdown(countdownId: key, cancel: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Sound
|
|
|
|
fileprivate func _playSound(timerId: String) {
|
|
|
|
let context = PersistenceController.shared.container.viewContext
|
|
|
|
var coolSound: Sound? = nil
|
|
let timer = context.object(stringId: timerId)
|
|
switch timer {
|
|
case let cd as Countdown:
|
|
coolSound = cd.coolSound
|
|
case let sw as Stopwatch:
|
|
coolSound = sw.coolSound
|
|
default:
|
|
break
|
|
}
|
|
|
|
if let coolSound {
|
|
self.playSound(coolSound)
|
|
AudioServicesPlayAlertSound(SystemSoundID(kSystemSoundID_Vibrate))
|
|
} else {
|
|
print("No sound to play!")
|
|
}
|
|
}
|
|
|
|
func playSound(_ sound: Sound) {
|
|
do {
|
|
let soundFile = try sound.soundFile()
|
|
let soundPlayer = SoundPlayer()
|
|
self.soundPlayer = soundPlayer
|
|
try soundPlayer.playSound(soundFile: soundFile, repeats: false)
|
|
} catch {
|
|
Logger.error(error)
|
|
// TODO: manage error
|
|
}
|
|
}
|
|
|
|
func stopSoundIfPossible() {
|
|
self.soundPlayer?.stop()
|
|
self.soundPlayer = nil
|
|
}
|
|
|
|
// MARK: - Intent
|
|
|
|
fileprivate func _createTimerIntent(_ timer: AbstractTimer) {
|
|
let intent = LaunchTimerIntent()
|
|
|
|
let invocationPhrase = NSLocalizedString("Launch %@", comment: "")
|
|
intent.suggestedInvocationPhrase = String(format: invocationPhrase, timer.displayName)
|
|
intent.timer = TimerIdentifier(identifier: timer.stringId, display: timer.displayName)
|
|
|
|
let interaction = INInteraction(intent: intent, response: nil)
|
|
interaction.donate()
|
|
}
|
|
|
|
// MARK: - Live Activity
|
|
|
|
fileprivate func _launchLiveActivity(stopwatch: Stopwatch, start: Date) {
|
|
|
|
if ActivityAuthorizationInfo().areActivitiesEnabled {
|
|
|
|
let contentState = LaunchWidgetAttributes.ContentState(ended: false)
|
|
let attributes = LaunchWidgetAttributes(id: stopwatch.stringId, name: stopwatch.displayName, date: start)
|
|
let activityContent = ActivityContent(state: contentState, staleDate: nil)
|
|
|
|
do {
|
|
let liveActivity = try ActivityKit.Activity.request(attributes: attributes, content: activityContent)
|
|
print("Requested a Live Activity: \(String(describing: liveActivity.id)).")
|
|
} catch (let error) {
|
|
Logger.error(error)
|
|
}
|
|
|
|
// self._scheduleAppRefresh(countdown: countdown)
|
|
|
|
}
|
|
}
|
|
|
|
fileprivate func _scheduleAppRefresh(countdown: Countdown) {
|
|
let request = BGAppRefreshTaskRequest(identifier: BGTaskIdentifier.refresh.rawValue)
|
|
request.earliestBeginDate = Date(timeIntervalSinceNow: countdown.duration)
|
|
do {
|
|
try BGTaskScheduler.shared.submit(request)
|
|
print("request submitted with date: \(String(describing: request.earliestBeginDate))")
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
fileprivate func _liveActivity(timerId: String) -> ActivityKit.Activity<LaunchWidgetAttributes>? {
|
|
return ActivityKit.Activity<LaunchWidgetAttributes>.activities.first(where: { $0.attributes.id == timerId } )
|
|
}
|
|
|
|
func updateLiveActivities() {
|
|
print("update live activity...")
|
|
|
|
for (countdownId, interval) in self.currentCountdowns {
|
|
|
|
if interval.end < Date() {
|
|
self._endLiveActivity(timerId: countdownId)
|
|
}
|
|
|
|
|
|
// if let activity = self._liveActivity(countdownId: countdownId) {
|
|
//
|
|
// Task {
|
|
//
|
|
// if ended {
|
|
// self._endLiveActivity(countdownId: countdownId)
|
|
// }
|
|
//
|
|
//// let state = LaunchWidgetAttributes.ContentState(ended: ended)
|
|
//// let content = ActivityContent(state: state, staleDate: interval.end)
|
|
//// await activity.update(content)
|
|
//// print("Ending the Live Activity: \(activity.id)")
|
|
// }
|
|
// }
|
|
}
|
|
|
|
}
|
|
|
|
fileprivate func _endLiveActivity(timerId: String) {
|
|
|
|
print("Try to end the Live Activity: \(timerId)")
|
|
|
|
if let activity = self._liveActivity(timerId: timerId) {
|
|
Task {
|
|
let state = LaunchWidgetAttributes.ContentState(ended: true)
|
|
let content = ActivityContent(state: state, staleDate: Date())
|
|
await activity.end(content, dismissalPolicy: .immediate)
|
|
print("Ending the Live Activity: \(activity.id)")
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|