diff --git a/Common/Notifications.h b/Common/Notifications.h index 2f4fccd..2136a0f 100644 --- a/Common/Notifications.h +++ b/Common/Notifications.h @@ -4,6 +4,7 @@ #define NOTIFICATION_NAME_ACTIVITY_STARTED "ActivityStarted" // The user has started an activity #define NOTIFICATION_NAME_ACTIVITY_STOPPED "ActivityStopped" // The user has stopped an activity #define NOTIFICATION_NAME_GEAR_LIST_UPDATED "GearListUpdated" // An updated gear list was returned from the (optional) server +#define NOTIFICATION_NAME_RACE_LIST_UPDATED "RaceListUpdated" // The race calendar from the (optional) server was updated #define NOTIFICATION_NAME_PLANNED_WORKOUTS_UPDATED "PlannedWorkoutsUpdated" // The planned workouts list from the (optional) server was updated #define NOTIFICATION_NAME_INTERVAL_SESSIONS_UPDATED "IntervalSessionsUpdated" // The interval sessions list from the (optional) server was updated #define NOTIFICATION_NAME_INTERVAL_UPDATED "IntervalUpdated" diff --git a/Common/Params.h b/Common/Params.h index f0ae640..c729c47 100644 --- a/Common/Params.h +++ b/Common/Params.h @@ -67,6 +67,13 @@ #define PARAM_WORKOUT_INTERVALS "intervals" #define PARAM_WORKOUT_LAST_UPDATED_TIME "last updated time" +// Goals +#define PARAM_RACE_ID "race id" +#define PARAM_RACE_DATE "race date" +#define PARAM_RACE_IMPORTANCE "race importance" +#define PARAM_RACE_DISTANCE "race distance" +#define PARAM_RACE_NAME "race name" + // Interval Session #define PARAM_INTERVAL_ID "id" #define PARAM_INTERVAL_NAME "name" diff --git a/Common/Urls.h b/Common/Urls.h index 55f1444..f747036 100644 --- a/Common/Urls.h +++ b/Common/Urls.h @@ -20,6 +20,7 @@ #define REMOTE_API_CREATE_GEAR_URL "api/1.0/create_gear" #define REMOTE_API_UPDATE_GEAR_URL "api/1.0/update_gear" #define REMOTE_API_DELETE_GEAR_URL "api/1.0/delete_gear" +#define REMOTE_API_LIST_RACES_URL "api/1.0/list_races" #define REMOTE_API_LIST_PLANNED_WORKOUTS_URL "api/1.0/list_planned_workouts" #define REMOTE_API_LIST_INTERVAL_WORKOUTS_URL "api/1.0/list_interval_workouts" #define REMOTE_API_LIST_PACE_PLANS_URL "api/1.0/list_pace_plans" diff --git a/IOS/Controller/CommonApp.swift b/IOS/Controller/CommonApp.swift index 20159b2..642c31b 100644 --- a/IOS/Controller/CommonApp.swift +++ b/IOS/Controller/CommonApp.swift @@ -81,6 +81,7 @@ class CommonApp : ObservableObject { NotificationCenter.default.addObserver(self, selector: #selector(self.requestUserSettingsResponse), name: Notification.Name(rawValue: NOTIFICATION_NAME_REQUEST_USER_SETTINGS_RESULT), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.downloadedActivityReceived), name: Notification.Name(rawValue: NOTIFICATION_NAME_DOWNLOADED_ACTIVITY), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.gearListUpdated), name: Notification.Name(rawValue: NOTIFICATION_NAME_GEAR_LIST_UPDATED), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.raceListUpdated), name: Notification.Name(rawValue: NOTIFICATION_NAME_RACE_LIST_UPDATED), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.plannedWorkoutsUpdated), name: Notification.Name(rawValue: NOTIFICATION_NAME_PLANNED_WORKOUTS_UPDATED), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.intervalSessionsUpdated), name: Notification.Name(rawValue: NOTIFICATION_NAME_INTERVAL_SESSIONS_UPDATED), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.pacePlansUpdated), name: Notification.Name(rawValue: NOTIFICATION_NAME_PACE_PLANS_UPDATED), object: nil) @@ -291,11 +292,11 @@ class CommonApp : ObservableObject { // Parse the URL. let components = URLComponents(url: requestUrl, resolvingAgainstBaseURL: false)! - + if let queryItems = components.queryItems { var activityId: String? var exportFormat: String? - + // Grab the activity ID and file format out of the URL parameters. for queryItem in queryItems { if queryItem.name == PARAM_ACTIVITY_ID { @@ -305,7 +306,7 @@ class CommonApp : ObservableObject { exportFormat = queryItem.value! } } - + if activityId != nil && exportFormat != nil { let directory = NSTemporaryDirectory() let fileName = NSUUID().uuidString + "." + exportFormat! @@ -313,14 +314,39 @@ class CommonApp : ObservableObject { if fullUrl != nil { try responseData.write(to: fullUrl!) - - if ImportActivityFromFile(fullUrl?.absoluteString, "", activityId) == false { + + if ImportActivityFromFile(fullUrl?.absoluteString, "", activityId) { + var startTime: time_t = 0 + var endTime: time_t = 0 + + LoadHistoricalActivity(activityId) + if GetHistoricalActivityStartAndEndTime(activityId, &startTime, &endTime) { + let lastSynchedActivityTime = Preferences.lastServerImportTime() + + if startTime > lastSynchedActivityTime { + Preferences.setLastServerImportTime(value: startTime) + } + } + } + else { NSLog("Import failed!") } try FileManager.default.removeItem(at: fullUrl!) } + else { + NSLog("Cannot find the downloaded file!") + } + } + else { + NSLog("Activity ID and Export Format not provided!") } } + else { + NSLog("Cannot parse downloaded file URL!") + } + } + else { + NSLog("Response URL or Response Data not provided!") } } } @@ -349,6 +375,26 @@ class CommonApp : ObservableObject { } } + @objc func raceListUpdated(notification: NSNotification) { + do { + if let data = notification.object as? Dictionary { + if let responseData = data[KEY_NAME_RESPONSE_DATA] as? Data { + let workoutsVM: WorkoutsVM = WorkoutsVM() + let raceList = try JSONSerialization.jsonObject(with: responseData, options: []) as! [Any] + + for race in raceList { + if let raceDict = race as? Dictionary { + workoutsVM.importRaceCalendar(dict: raceDict) + } + } + } + } + } + catch { + NSLog(error.localizedDescription) + } + } + @objc func plannedWorkoutsUpdated(notification: NSNotification) { do { if let data = notification.object as? Dictionary { diff --git a/IOS/Model/ApiClient.swift b/IOS/Model/ApiClient.swift index e45cdd5..aee253a 100644 --- a/IOS/Model/ApiClient.swift +++ b/IOS/Model/ApiClient.swift @@ -133,6 +133,10 @@ class ApiClient : ObservableObject { let notification = Notification(name: Notification.Name(rawValue: NOTIFICATION_NAME_GEAR_LIST_UPDATED), object: downloadedData) NotificationCenter.default.post(notification) } + else if url.contains(REMOTE_API_LIST_RACES_URL) { + let notification = Notification(name: Notification.Name(rawValue: NOTIFICATION_NAME_RACE_LIST_UPDATED), object: downloadedData) + NotificationCenter.default.post(notification) + } else if url.contains(REMOTE_API_LIST_PLANNED_WORKOUTS_URL) { let notification = Notification(name: Notification.Name(rawValue: NOTIFICATION_NAME_PLANNED_WORKOUTS_UPDATED), object: downloadedData) NotificationCenter.default.post(notification) @@ -292,6 +296,11 @@ class ApiClient : ObservableObject { return self.makeRequest(url: urlStr, method: "DELETE", data: deleteDict) } + func listRaces() -> Bool { + let urlStr = self.buildApiUrlStr(request: REMOTE_API_LIST_RACES_URL) + return self.makeRequest(url: urlStr, method: "GET", data: [:]) + } + func listPlannedWorkouts() -> Bool { let urlStr = self.buildApiUrlStr(request: REMOTE_API_LIST_PLANNED_WORKOUTS_URL) return self.makeRequest(url: urlStr, method: "GET", data: [:]) @@ -634,12 +643,14 @@ class ApiClient : ObservableObject { if now - lastServerSync > 60 { let deviceId = Preferences.uuid() if deviceId != nil { + // Associate this device with the user. result = self.claimDevice(deviceId: deviceId!) // Get all the things. #if !os(watchOS) result = result && self.listGear() + result = result && self.listRaces() result = result && self.listPlannedWorkouts() result = result && self.requestUserSettings(settings: [WORKOUT_INPUT_GOAL_TYPE]) #endif @@ -653,14 +664,9 @@ class ApiClient : ObservableObject { result = result && self.sendPacePlansToServer() #endif - // Ask for the server's activity list from the last week. - let ONE_WEEK = 60 * 60 * 24 * 7 - if lastServerSync > ONE_WEEK { - result = result && self.requestUpdatesSince(timestamp: Date(timeIntervalSince1970: TimeInterval(lastServerSync - ONE_WEEK))) - } - else { - result = result && self.requestUpdatesSince(timestamp: Date(timeIntervalSince1970: 0)) - } + // Ask for the server's activity list, from the time of the last activity received. + let lastServerImport = Preferences.lastServerImportTime() + result = result && self.requestUpdatesSince(timestamp: Date(timeIntervalSince1970: TimeInterval(lastServerImport))) Preferences.setLastServerSyncTime(value: now) } diff --git a/IOS/Model/Preferences.swift b/IOS/Model/Preferences.swift index 632f3b4..68b2166 100644 --- a/IOS/Model/Preferences.swift +++ b/IOS/Model/Preferences.swift @@ -62,6 +62,7 @@ let PREF_NAME_WORKOUTS_CAN_INCLUDE_RUNNING = "Workouts Can Include Runn let PREF_NAME_POOL_LENGTH = "Pool Length" let PREF_NAME_POOL_LENGTH_UNITS = "Pool Length Units" let PREF_NAME_LAST_SERVER_SYNC_TIME = "Last Server Sync Time" +let PREF_NAME_LAST_SERVER_IMPORT_TIME = "Last Server Import Time" let PREF_NAME_MOST_RECENT_ACTIVITY_DESCRIPTION = "Most Recent Activity Description" let PREF_NAME_METRIC = "units_metric" @@ -560,7 +561,12 @@ class Preferences { let mydefaults: UserDefaults = UserDefaults.standard return mydefaults.integer(forKey: PREF_NAME_LAST_SERVER_SYNC_TIME) } - + + static func lastServerImportTime() -> time_t { + let mydefaults: UserDefaults = UserDefaults.standard + return mydefaults.integer(forKey: PREF_NAME_LAST_SERVER_IMPORT_TIME) + } + // // Set methods // @@ -844,7 +850,12 @@ class Preferences { let mydefaults: UserDefaults = UserDefaults.standard mydefaults.set(value, forKey: PREF_NAME_LAST_SERVER_SYNC_TIME) } - + + static func setLastServerImportTime(value: time_t) { + let mydefaults: UserDefaults = UserDefaults.standard + mydefaults.set(value, forKey: PREF_NAME_LAST_SERVER_IMPORT_TIME) + } + // // Methods for managing the list of accessories // diff --git a/View Models/WorkoutsVM.swift b/View Models/WorkoutsVM.swift index 054c248..604e250 100644 --- a/View Models/WorkoutsVM.swift +++ b/View Models/WorkoutsVM.swift @@ -155,13 +155,32 @@ class WorkoutsVM : ObservableObject { func importWorkoutFromDict(dict: Dictionary) throws { if let workoutId = dict[PARAM_WORKOUT_ID] as? String, let workoutTypeStr = dict[PARAM_WORKOUT_WORKOUT_TYPE] as? String, - let activityType = dict[PARAM_WORKOUT_ACTIVITY_TYPE] as? String, + let activityTypeStr = dict[PARAM_WORKOUT_ACTIVITY_TYPE] as? String, let scheduledTime = dict[PARAM_WORKOUT_SCHEDULED_TIME] as? time_t { let estimatedIntensityScore = dict[PARAM_WORKOUT_ESTIMATED_INTENSITY] as? Double ?? 0.0 let workoutType = WorkoutTypeStrToEnum(workoutTypeStr) - CreateWorkout(workoutId, workoutType, activityType, estimatedIntensityScore, scheduledTime) + CreateWorkout(workoutId, workoutType, activityTypeStr, estimatedIntensityScore, scheduledTime) + } + } + + func importRaceCalendar(dict: Dictionary) { + if let raceDate = dict[PARAM_RACE_DATE] as? time_t, + let raceDistance = dict[PARAM_RACE_DISTANCE] as? String { + + // Ignore anything that has already passed. + let now = Date() + if raceDate > time_t(now.timeIntervalSince1970) { + let goal = WorkoutsVM.workoutGoalStringToEnum(goalStr: raceDistance) + let currentGoal = Preferences.workoutGoal() + let currentGoalDate = Preferences.workoutGoalDate() + + if currentGoal == GOAL_FITNESS || raceDate < currentGoalDate { + Preferences.setWorkoutGoal(value: goal) + Preferences.setWorkoutGoalDate(value: raceDate) + } + } } } diff --git a/iPhone App/HistoryView.swift b/iPhone App/HistoryView.swift index 164bc6c..93d64c3 100644 --- a/iPhone App/HistoryView.swift +++ b/iPhone App/HistoryView.swift @@ -7,6 +7,7 @@ import SwiftUI struct HistoryView: View { @ObservedObject private var historyVM = HistoryVM() + @State var displayedDates : Set = [] let dateFormatter: DateFormatter = { let df = DateFormatter() @@ -17,6 +18,9 @@ struct HistoryView: View { private func loadHistory() { DispatchQueue.global(qos: .userInitiated).async { + if let updatesSince = self.displayedDates.min() { + let _ = ApiClient.shared.requestUpdatesSince(timestamp: updatesSince) + } self.historyVM.buildHistoricalActivitiesList(createAllObjects: false) } } @@ -62,6 +66,10 @@ struct HistoryView: View { } .onAppear() { item.requestMetadata() + self.displayedDates.insert(item.startTime) + } + .onDisappear { + self.displayedDates.remove(item.startTime) } } }