From b54012905853e94943721e0389a7cebe63edf9d7 Mon Sep 17 00:00:00 2001 From: liamcharger Date: Sun, 26 Jan 2025 15:10:33 -0500 Subject: [PATCH] multiple: remove references to MusicController in exercises and begin implementation of heart range notifications --- .DS_Store | Bin 10244 -> 10244 bytes InfiniLink/BLE/BLECharacteristicHandler.swift | 5 +- InfiniLink/Core/Components/ActionView.swift | 2 +- .../View Model/ExerciseViewModel.swift | 14 -- .../Exercise/Views/ActiveExerciseView.swift | 39 ----- .../Exercise/Views/ExerciseDetailView.swift | 22 --- .../General/GeneralSettingsView.swift | 11 -- .../NotificationsSettingsView.swift | 20 ++- .../InfiniLink.xcdatamodel/contents | 12 +- InfiniLink/Localizable.xcstrings | 24 ++- InfiniLink/Utils/MusicController.swift | 161 +++++++++--------- InfiniLink/Utils/NotificationManager.swift | 29 ++++ 12 files changed, 149 insertions(+), 190 deletions(-) diff --git a/.DS_Store b/.DS_Store index 9e0a4ec601cd7054403303f33ef7d3338552c333..196c142eb0f242c140a5d18b23e9be6ebcbc0a13 100644 GIT binary patch delta 53 zcmZn(XbG6$F8U^hRb&SoBgJSKiaOUqgvg=$N4104lZL&M4EgpD`v5mMybSf<6Y JnO)&8I{>W!59$B_ delta 80 zcmZn(XbG6$dGU^hRb?q(i=JSKi43&UC+g=$M9104kuW3$QUgpD`v5mMw7!e4d(aF!MR diff --git a/InfiniLink/BLE/BLECharacteristicHandler.swift b/InfiniLink/BLE/BLECharacteristicHandler.swift index df314c1..3d4a448 100644 --- a/InfiniLink/BLE/BLECharacteristicHandler.swift +++ b/InfiniLink/BLE/BLECharacteristicHandler.swift @@ -27,7 +27,7 @@ struct BLECharacteristicHandler { @AppStorage("lastHeartRateUpdateTimestamp") var lastHeartRateUpdateTimestamp: Double = 0 @AppStorage("lastTimeCheckCompleted") var lastTimeCheckCompleted: Double = 0 - @AppStorage("lastTimeStepGoalNotified") var lastTimeStepGoalNotified: Double = 86400 + @AppStorage("lastTimeStepGoalNotified") var lastTimeStepGoalNotified: Double = 0 func fetchHeartPoints() -> [HeartDataPoint] { let fetchRequest: NSFetchRequest = HeartDataPoint.fetchRequest() @@ -234,7 +234,10 @@ struct BLECharacteristicHandler { } private func updateHeartRate(bpm: Int) { lastHeartRateUpdateTimestamp = Date().timeIntervalSince1970 + healthKitManager.writeHeartRate(date: Date(), dataToAdd: bleManager.heartRate) chartManager.addHeartRateDataPoint(heartRate: Double(bpm), time: Date()) + + notificationManager.sendHeartRangeNotification(bpm) } } diff --git a/InfiniLink/Core/Components/ActionView.swift b/InfiniLink/Core/Components/ActionView.swift index 414f835..7f39c57 100644 --- a/InfiniLink/Core/Components/ActionView.swift +++ b/InfiniLink/Core/Components/ActionView.swift @@ -45,7 +45,7 @@ struct ActionView: View { .padding(10) .font(.body.weight(.semibold)) .background(action.accent) - .foregroundStyle(.white) // TODO: check this works for all accents + .foregroundStyle(.white) .clipShape(RoundedRectangle(cornerRadius: 10)) Text(NSLocalizedString(action.title, comment: "")) .font(.system(size: 28).weight(.bold)) diff --git a/InfiniLink/Core/Exercise/View Model/ExerciseViewModel.swift b/InfiniLink/Core/Exercise/View Model/ExerciseViewModel.swift index 698bf65..7f87f9d 100644 --- a/InfiniLink/Core/Exercise/View Model/ExerciseViewModel.swift +++ b/InfiniLink/Core/Exercise/View Model/ExerciseViewModel.swift @@ -14,7 +14,6 @@ class ExerciseViewModel: ObservableObject { let healthKitManager = HealthKitManager.shared - @Published var playedTracks = [PlayedTrack]() @Published var exerciseTime: TimeInterval = 0 @Published var stepsTaken: Int = 0 @Published var currentExercise: Exercise? @@ -60,18 +59,6 @@ class ExerciseViewModel: ObservableObject { } } - func addTrackToExercise(title: String, artist: String) { - let track = PlayedTrack(context: PersistenceController.shared.container.viewContext) - track.id = UUID() - track.timestamp = Date() - track.title = title - track.artist = artist - - if !playedTracks.contains(where: { $0.title == track.title && $0.artist == track.artist }) { - playedTracks.append(track) - } - } - func reset() { stepsTaken = 0 exerciseTime = 0 @@ -97,7 +84,6 @@ class ExerciseViewModel: ObservableObject { newExercise.startDate = startDate newExercise.endDate = Date() newExercise.exerciseId = exercise - newExercise.playedTracks = NSSet(array: playedTracks) newExercise.heartPoints = NSSet(array: heartPoints) newExercise.steps = Int32(stepsTaken) diff --git a/InfiniLink/Core/Exercise/Views/ActiveExerciseView.swift b/InfiniLink/Core/Exercise/Views/ActiveExerciseView.swift index 46b8008..03b61fc 100644 --- a/InfiniLink/Core/Exercise/Views/ActiveExerciseView.swift +++ b/InfiniLink/Core/Exercise/Views/ActiveExerciseView.swift @@ -14,7 +14,6 @@ struct ActiveExerciseView: View { @Environment(\.managedObjectContext) var viewContext @ObservedObject var exerciseViewModel = ExerciseViewModel.shared - @ObservedObject var musicController = MusicController.shared @ObservedObject var bleManager = BLEManager.shared @Binding var exercise: Exercise? @@ -55,43 +54,6 @@ struct ActiveExerciseView: View { } } Spacer() - if musicController.musicPlaying != 0 { - HStack(spacing: 12) { - if let artwork = musicController.musicPlayer.nowPlayingItem?.artwork, let image = artwork.image(at: CGSize(width: 52, height: 52)) { - Image(uiImage: image) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 52, height: 52) - .clipShape(RoundedRectangle(cornerRadius: 10)) - } - VStack(alignment: .leading, spacing: 2) { - Text(musicController.musicPlayer.nowPlayingItem?.title ?? "Not Playing") - .fontWeight(.bold) - if let artist = musicController.musicPlayer.nowPlayingItem?.artist { - Text(artist) - } - } - Spacer() - HStack(spacing: 16) { - Button { - musicController.musicPlaying == 1 ? musicController.pause() : musicController.play() - } label: { - Image(systemName: musicController.musicPlaying == 1 ? "pause.fill" : "play.fill") - .font(.system(size: 21)) - } - Button { - musicController.skipForward() - } label: { - Image(systemName: "forward.fill") - .font(.system(size: 22)) - } - } - } - .padding(14) - .foregroundStyle(Color.primary) - .background(Material.regular) - .clipShape(RoundedRectangle(cornerRadius: 20)) - } HStack(spacing: 14) { Spacer() Button { @@ -145,7 +107,6 @@ struct ActiveExerciseView: View { currentStepCount = bleManager.stepCount DispatchQueue.main.async { startTimer() - musicController.updateMusicInformation() } } // Should these onChanges go in BLECharacteristicHandler? diff --git a/InfiniLink/Core/Exercise/Views/ExerciseDetailView.swift b/InfiniLink/Core/Exercise/Views/ExerciseDetailView.swift index 33e6126..4efdd0f 100644 --- a/InfiniLink/Core/Exercise/Views/ExerciseDetailView.swift +++ b/InfiniLink/Core/Exercise/Views/ExerciseDetailView.swift @@ -93,13 +93,6 @@ struct ExerciseDetailView: View { .foregroundStyle(.gray) } } - if let tracks = userExercise.playedTracks, tracks.count > 1 { - HStack { - Text("Total Tracks Played") - Text("\(tracks.count)") - .foregroundStyle(.gray) - } - } } if heartPoints.count > 1 { Section("Heart Rate") { @@ -107,21 +100,6 @@ struct ExerciseDetailView: View { } .listRowBackground(Color.clear) } - if let tracksSet = userExercise.playedTracks as? Set, tracksSet.count > 1 { - let tracks = Array(tracksSet).sorted(by: { $0.timestamp ?? Date() < $1.timestamp ?? Date() }) - - Section("Played Tracks") { - ForEach(tracks) { track in - if let title = track.title, let artist = track.artist { - VStack(alignment: .leading, spacing: 5) { - Text(title) - .fontWeight(.bold) - Text(artist) - } - } - } - } - } } } } diff --git a/InfiniLink/Core/Settings/General/GeneralSettingsView.swift b/InfiniLink/Core/Settings/General/GeneralSettingsView.swift index 0439177..c147a52 100644 --- a/InfiniLink/Core/Settings/General/GeneralSettingsView.swift +++ b/InfiniLink/Core/Settings/General/GeneralSettingsView.swift @@ -36,17 +36,6 @@ struct GeneralSettingsView: View { } .disabled(bleManager.blefsTransfer == nil) } - Section { - // TODO: implement non-dummy input form in SetUpDetailsView - /* - NavigationLink { - SetUpDetailsView(listOnly: true) - .navigationBarTitle("Health Details") - } label: { - Text("Health Details") - } - */ - } Section { NavigationLink { AppearanceView() diff --git a/InfiniLink/Core/Settings/Notifications/NotificationsSettingsView.swift b/InfiniLink/Core/Settings/Notifications/NotificationsSettingsView.swift index b2d16bd..3cc83e7 100644 --- a/InfiniLink/Core/Settings/Notifications/NotificationsSettingsView.swift +++ b/InfiniLink/Core/Settings/Notifications/NotificationsSettingsView.swift @@ -15,6 +15,9 @@ struct NotificationsSettingsView: View { @AppStorage("waterReminder") var waterReminder = true @AppStorage("waterReminderAmount") var waterReminderAmount = 7 @AppStorage("standUpReminder") var standUpReminder = true + @AppStorage("heartRangeReminder") var heartRangeReminder = true + @AppStorage("minHeartRange") var minHeartRange = 40 + @AppStorage("maxHeartRange") var maxHeartRange = 150 @AppStorage("watchNotifications") var watchNotifications = true @AppStorage("enableReminders") var enableReminders = true @AppStorage("enableCalendarNotifications") var enableCalendarNotifications = true @@ -49,7 +52,7 @@ struct NotificationsSettingsView: View { Toggle("Water Reminder", isOn: $waterReminder) if waterReminder { Picker("Interval", selection: $waterReminderAmount) { - ForEach(0..<9) { amount in + ForEach(0..<9, id: \.self) { amount in Text("\(amount + 1) time\(amount == 0 ? "" : "s")") } } @@ -60,6 +63,21 @@ struct NotificationsSettingsView: View { Toggle("Stand-up Reminder", isOn: $standUpReminder) } */ + Section(footer: Text("Get a notification whenn your heart rate goes above or below the specified range.")) { + Toggle("Heart Range Notifications", isOn: $heartRangeReminder) + if heartRangeReminder { + HStack { + Text("Minimum") + Stepper("\(minHeartRange)", value: $minHeartRange, in: 40...(maxHeartRange - 1), step: 1) + .fontWeight(.semibold) + } + HStack { + Text("Maximum") + Stepper("\(maxHeartRange)", value: $maxHeartRange, in: (minHeartRange + 1)...220, step: 1) + .fontWeight(.semibold) + } + } + } Section(header: Text("Daily Goals"), footer: Text("Get notified when you reach your daily fitness goals.")) { Toggle("Steps", isOn: $remindOnStepGoalCompletion) } diff --git a/InfiniLink/InfiniLink.xcdatamodeld/InfiniLink.xcdatamodel/contents b/InfiniLink/InfiniLink.xcdatamodeld/InfiniLink.xcdatamodel/contents index 095c946..ad6aaf1 100644 --- a/InfiniLink/InfiniLink.xcdatamodeld/InfiniLink.xcdatamodel/contents +++ b/InfiniLink/InfiniLink.xcdatamodeld/InfiniLink.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -51,13 +51,6 @@ - - - - - - - @@ -77,6 +70,5 @@ - - + \ No newline at end of file diff --git a/InfiniLink/Localizable.xcstrings b/InfiniLink/Localizable.xcstrings index 49e563c..95916d1 100644 --- a/InfiniLink/Localizable.xcstrings +++ b/InfiniLink/Localizable.xcstrings @@ -408,6 +408,9 @@ }, "General" : { + }, + "Get a notification whenn your heart rate goes above or below the specified range." : { + }, "Get notifications on your watch when you reach goals, when it's time to drink water, and more." : { @@ -441,9 +444,18 @@ }, "Health Details" : { + }, + "Heart Range Notifications" : { + }, "Heart Rate" : { + }, + "Heart Rate High" : { + + }, + "Heart Rate Low" : { + }, "Height" : { @@ -525,9 +537,15 @@ }, "M" : { + }, + "Maximum" : { + }, "Metric" : { + }, + "Minimum" : { + }, "Music" : { @@ -579,9 +597,6 @@ }, "Pair New Device" : { - }, - "Played Tracks" : { - }, "Pull Request #2217 on GitHub" : { @@ -711,9 +726,6 @@ }, "Total Steps" : { - }, - "Total Tracks Played" : { - }, "Units" : { diff --git a/InfiniLink/Utils/MusicController.swift b/InfiniLink/Utils/MusicController.swift index f10b025..9ebde5a 100644 --- a/InfiniLink/Utils/MusicController.swift +++ b/InfiniLink/Utils/MusicController.swift @@ -10,53 +10,45 @@ import MediaPlayer import NotificationCenter import SwiftUI -class MusicController: ObservableObject { - static let shared = MusicController() +class MusicController { + static let shared = MusicController() - @Published var musicPlaying = 0 + let bleManager = BLEManager.shared + + var musicPlayer = MPMusicPlayerController.systemMusicPlayer + var musicPlaying = 0 - let bleManager = BLEManager.shared - let musicPlayer = MPMusicPlayerController.systemMusicPlayer let volumeSlots: Float = 15.0 - - enum MusicState { - case play, pause, nextTrack, prevTrack - } + + struct SongInfo { + var trackName: String! + var artistName: String! + } + + enum MusicState { + case play, pause, nextTrack, prevTrack + } @AppStorage("allowMusicControl") var allowMusicControl = true @AppStorage("allowVolumeControl") var allowVolumeControl = true - private init() { + private init() { initialize() - } - - @objc func onNotificationReceipt(_ notification: NSNotification) { - updateMusicInformation() - } + } + + @objc func onNotificationReceipt(_ notification: NSNotification) { + musicPlaying = musicPlayer.playbackState.rawValue + updateMusicInformation(songInfo: getCurrentSongInfo()) + } func initialize() { musicPlayer.beginGeneratingPlaybackNotifications() NotificationCenter.default.addObserver(self, selector: #selector(self.onNotificationReceipt(_:)), name: .MPMusicPlayerControllerPlaybackStateDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.onNotificationReceipt(_:)), name: .MPMusicPlayerControllerNowPlayingItemDidChange, object: nil) } - - func play() { - musicPlayer.play() - musicPlaying = 1 - } - func pause() { - musicPlayer.pause() - musicPlaying = 2 - } - func skipForward() { - musicPlayer.skipToNextItem() - } - func skipToPrevious() { - musicPlayer.skipToPreviousItem() - } - func controlMusic(controlNumber: Int) { - if allowMusicControl { + func controlMusic(controlNumber: Int) { + if allowMusicControl { // When CoreBluetooth gets an update from the music control characteristic, parse that number and take an action, and in any case, make sure the track and artist are up-to-date let session = AVAudioSession.sharedInstance() @@ -67,67 +59,66 @@ class MusicController: ObservableObject { } catch { log("Unable to activate audio session: \(error.localizedDescription)", caller: "MusicController") } + musicPlaying = musicPlayer.playbackState.rawValue - DispatchQueue.main.async { - self.musicPlaying = self.musicPlayer.playbackState.rawValue - - switch controlNumber { - case 0: - self.play() - case 1: - self.pause() - case 3: - self.skipForward() - case 4: - self.skipToPrevious() - case 5: - if self.allowVolumeControl { - let newVolume = min(session.outputVolume + (1 / self.volumeSlots), 1.0) - MPVolumeView.setVolume(newVolume) - } - case 6: - if self.allowVolumeControl { - let newVolume = max(session.outputVolume - (1 / self.volumeSlots), 0.0) - MPVolumeView.setVolume(newVolume) - } - case 224: - self.updateMusicInformation() - default: - break + switch controlNumber { + case 0: + musicPlayer.play() + musicPlaying = 1 + case 1: + musicPlayer.pause() + musicPlaying = 2 + case 3: + musicPlayer.skipToNextItem() + case 4: + musicPlayer.skipToPreviousItem() + case 5: + if allowVolumeControl { + let newVolume = min(session.outputVolume + (1 / volumeSlots), 1.0) + MPVolumeView.setVolume(newVolume) + } + case 6: + if allowVolumeControl { + let newVolume = max(session.outputVolume - (1 / volumeSlots), 0.0) + MPVolumeView.setVolume(newVolume) } - self.updateMusicInformation() + default: + break } + + updateMusicInformation(songInfo: getCurrentSongInfo()) } - } - - func updateMusicInformation() { + } + + + func getCurrentSongInfo() -> SongInfo { + let currentTrack = musicPlayer.nowPlayingItem + let currentSongInfo = SongInfo(trackName: currentTrack?.title ?? "Not Playing", artistName: currentTrack?.artist ?? "") + return currentSongInfo + } + + func updateMusicInformation(songInfo: MusicController.SongInfo) { let bleWriteManager = BLEWriteManager() - - DispatchQueue.main.async { [self] in - musicPlaying = musicPlayer.playbackState.rawValue - - if let title = musicPlayer.nowPlayingItem?.title, let artist = musicPlayer.nowPlayingItem?.artist { - ExerciseViewModel.shared.addTrackToExercise(title: title, artist: artist) - } - - bleWriteManager.writeToMusicApp(message: musicPlayer.nowPlayingItem?.title ?? "Not Playing", characteristic: bleManager.musicChars.track) - bleWriteManager.writeToMusicApp(message: musicPlayer.nowPlayingItem?.artist ?? "", characteristic: bleManager.musicChars.artist) - - var playbackTime = musicPlayer.currentPlaybackTime; if playbackTime == musicPlayer.nowPlayingItem?.playbackDuration {playbackTime = 0.0} - bleWriteManager.writeHexToMusicApp(message: convertTime(value: playbackTime), characteristic: bleManager.musicChars.position) - bleWriteManager.writeHexToMusicApp(message: convertTime(value: musicPlayer.nowPlayingItem?.playbackDuration ?? 0.0), characteristic: bleManager.musicChars.length) - - if musicPlaying == 1 { - bleWriteManager.writeHexToMusicApp(message: [0x01], characteristic: bleManager.musicChars.status) - } else { - bleWriteManager.writeHexToMusicApp(message: [0x00], characteristic: bleManager.musicChars.status) - } + + let songInfo = getCurrentSongInfo() + + bleWriteManager.writeToMusicApp(message: songInfo.trackName, characteristic: bleManager.musicChars.track) + bleWriteManager.writeToMusicApp(message: songInfo.artistName, characteristic: bleManager.musicChars.artist) + + var playbackTime = musicPlayer.currentPlaybackTime; if playbackTime == musicPlayer.nowPlayingItem?.playbackDuration {playbackTime = 0.0} + bleWriteManager.writeHexToMusicApp(message: convertTime(value: playbackTime), characteristic: bleManager.musicChars.position) + bleWriteManager.writeHexToMusicApp(message: convertTime(value: musicPlayer.nowPlayingItem?.playbackDuration ?? 0.0), characteristic: bleManager.musicChars.length) + + if musicPlaying == 1 { + bleWriteManager.writeHexToMusicApp(message: [0x01], characteristic: bleManager.musicChars.status) + } else { + bleWriteManager.writeHexToMusicApp(message: [0x00], characteristic: bleManager.musicChars.status) } - } + } func convertTime(value: Double) -> [UInt8] { let val32 : UInt32 = UInt32(floor(value)) - + let byte1 = UInt8(val32 & 0x000000FF) let byte2 = UInt8((val32 & 0x0000FF00) >> 8) let byte3 = UInt8((val32 & 0x00FF0000) >> 16) @@ -141,7 +132,7 @@ extension MPVolumeView { static func setVolume(_ volume: Float) { let volumeView = MPVolumeView() let slider = volumeView.subviews.first(where: { $0 is UISlider }) as? UISlider - + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01) { slider?.value = volume } diff --git a/InfiniLink/Utils/NotificationManager.swift b/InfiniLink/Utils/NotificationManager.swift index d0ca011..bb0726d 100644 --- a/InfiniLink/Utils/NotificationManager.swift +++ b/InfiniLink/Utils/NotificationManager.swift @@ -38,6 +38,12 @@ class NotificationManager: ObservableObject { @AppStorage("waterReminderAmount") var waterReminderAmount = 7 @AppStorage("waterReminder") var waterReminder = true + @AppStorage("minHeartRange") var minHeartRange = 40 + @AppStorage("maxHeartRange") var maxHeartRange = 150 + @AppStorage("heartRangeReminder") var heartRangeReminder = true + @AppStorage("lastTimeMinHeartRangeNotified") var lastTimeMinHeartRangeNotified: Double = 0 + @AppStorage("lastTimeMaxHeartRangeNotified") var lastTimeMaxHeartRangeNotified: Double = 0 + private var nextReminderCheckDate: Date? private var waterReminderStartHour: Int = 8 private var waterReminderEndHour: Int = 20 @@ -105,7 +111,30 @@ extension NotificationManager { } } +// MARK: Health extension NotificationManager { + func sendHeartRangeNotification(_ bpm: Int) { + let date = Date().formatted(.dateTime.hour().minute()) + let currentTime = Date().timeIntervalSince1970 + + // FIXME: closures are being executed, but notifications are not being sent + + if bpm < minHeartRange, (currentTime - lastTimeMinHeartRangeNotified) >= (60 * 10) { + self.bleWriteManager.sendNotification( + AppNotification(title: NSLocalizedString("Heart Rate Low", comment: ""), subtitle: NSLocalizedString("Your heart rate fell below \(minHeartRange) BPM at \(date)", comment: "")) + ) + self.lastTimeMinHeartRangeNotified = currentTime + print("Triggered min check") + } + if bpm > maxHeartRange, (currentTime - lastTimeMaxHeartRangeNotified) >= (60 * 10) { + self.bleWriteManager.sendNotification( + AppNotification(title: NSLocalizedString("Heart Rate High", comment: ""), subtitle: NSLocalizedString("Your heart rate rose above \(maxHeartRange) BPM at \(date)", comment: "")) + ) + self.lastTimeMaxHeartRangeNotified = currentTime + print("Triggered max check") + } + } + func setWaterRemindersPerDay() { waterReminderAmount = waterReminderAmount