diff --git a/xdrip.xcodeproj/project.pbxproj b/xdrip.xcodeproj/project.pbxproj index aa73d002..a5c5801d 100644 --- a/xdrip.xcodeproj/project.pbxproj +++ b/xdrip.xcodeproj/project.pbxproj @@ -237,6 +237,7 @@ F85DC2F321CFE3D400B9F74A /* Calibration+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85DC2F021CFE3D400B9F74A /* Calibration+CoreDataClass.swift */; }; F85DC2F421CFE3D400B9F74A /* Sensor+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85DC2F121CFE3D400B9F74A /* Sensor+CoreDataClass.swift */; }; F85DC2F521CFE3D400B9F74A /* BgReading+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85DC2F221CFE3D400B9F74A /* BgReading+CoreDataClass.swift */; }; + F85FF39125288870004E6FF1 /* HouseKeeper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85FF39025288870004E6FF1 /* HouseKeeper.swift */; }; F867E2612252ADAB000FD265 /* Calibration+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F867E25D2252ADAB000FD265 /* Calibration+CoreDataProperties.swift */; }; F8691888239CEEFA0065B607 /* BluetoothPeripheralViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8691887239CEEFA0065B607 /* BluetoothPeripheralViewModel.swift */; }; F869188C23A044340065B607 /* TextsM5StackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F869188B23A044340065B607 /* TextsM5StackView.swift */; }; @@ -408,6 +409,7 @@ F8E53FCE251D35FB00052CE5 /* Common.strings in Resources */ = {isa = PBXBuildFile; fileRef = F8E53FD0251D35FB00052CE5 /* Common.strings */; }; F8E53FE9251F7B8800052CE5 /* Texts.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E53FE8251F7B8800052CE5 /* Texts.swift */; }; F8E540462521227000052CE5 /* xdrip.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F8E5403F2521227000052CE5 /* xdrip.xcdatamodeld */; }; + F8E5404C2522624800052CE5 /* ConstantsHousekeeping.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5404B2522624800052CE5 /* ConstantsHousekeeping.swift */; }; F8E6C78C24CDDB83007C1199 /* SnoozeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E6C78B24CDDB83007C1199 /* SnoozeViewController.swift */; }; F8E6C79024CEC22A007C1199 /* TextsSnooze.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E6C78F24CEC22A007C1199 /* TextsSnooze.swift */; }; F8E6C79124CEC2E3007C1199 /* Snooze.strings in Resources */ = {isa = PBXBuildFile; fileRef = F8E6C79324CEC2E3007C1199 /* Snooze.strings */; }; @@ -742,6 +744,7 @@ F85DC2F021CFE3D400B9F74A /* Calibration+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Calibration+CoreDataClass.swift"; sourceTree = ""; }; F85DC2F121CFE3D400B9F74A /* Sensor+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Sensor+CoreDataClass.swift"; sourceTree = ""; }; F85DC2F221CFE3D400B9F74A /* BgReading+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BgReading+CoreDataClass.swift"; sourceTree = ""; }; + F85FF39025288870004E6FF1 /* HouseKeeper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HouseKeeper.swift; sourceTree = ""; }; F867E25D2252ADAB000FD265 /* Calibration+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Calibration+CoreDataProperties.swift"; path = "xdrip/Core Data/extensions/Calibration+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; }; F8691887239CEEFA0065B607 /* BluetoothPeripheralViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothPeripheralViewModel.swift; sourceTree = ""; }; F869188B23A044340065B607 /* TextsM5StackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextsM5StackView.swift; sourceTree = ""; }; @@ -1016,6 +1019,7 @@ F8E540432521227000052CE5 /* xdrip v11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "xdrip v11.xcdatamodel"; sourceTree = ""; }; F8E540442521227000052CE5 /* xdrip v9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "xdrip v9.xcdatamodel"; sourceTree = ""; }; F8E540452521227000052CE5 /* xdrip v12.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "xdrip v12.xcdatamodel"; sourceTree = ""; }; + F8E5404B2522624800052CE5 /* ConstantsHousekeeping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsHousekeeping.swift; sourceTree = ""; }; F8E6C78B24CDDB83007C1199 /* SnoozeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozeViewController.swift; sourceTree = ""; }; F8E6C78F24CEC22A007C1199 /* TextsSnooze.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextsSnooze.swift; sourceTree = ""; }; F8E6C79224CEC2E3007C1199 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Snooze.strings; sourceTree = ""; }; @@ -1592,6 +1596,14 @@ path = "Application Delegate"; sourceTree = ""; }; + F85FF38F25288860004E6FF1 /* HouseKeeping */ = { + isa = PBXGroup; + children = ( + F85FF39025288870004E6FF1 /* HouseKeeper.swift */, + ); + path = HouseKeeping; + sourceTree = ""; + }; F870D3D425126A49008967B0 /* xDrip4iOS Widget */ = { isa = PBXGroup; children = ( @@ -2080,7 +2092,6 @@ F8EA6C7D21B70DEA0082976B /* Constants */ = { isa = PBXGroup; children = ( - F8252866243E50FE0067AF77 /* ConstantsLibre.swift */, F8A1585022EDB597007F5B5D /* ConstantsBGGraphBuilder.swift */, F8A1585222EDB602007F5B5D /* ConstantsBloodGlucose.swift */, F856CE5A22EDC8E50083E436 /* ConstantsBluetoothPairing.swift */, @@ -2093,17 +2104,19 @@ F8A1587222EDC893007F5B5D /* ConstantsDexcomShare.swift */, F80859262364355F00F3829D /* ConstantsGlucoseChart.swift */, F8A1586622EDB8BF007F5B5D /* ConstantsHomeView.swift */, + F8E5404B2522624800052CE5 /* ConstantsHousekeeping.swift */, + F8252866243E50FE0067AF77 /* ConstantsLibre.swift */, F8A1585E22EDB81E007F5B5D /* ConstantsLog.swift */, F8A389C723203E3E0010F405 /* ConstantsM5Stack.swift */, F8A1586A22EDB967007F5B5D /* ConstantsMaster.swift */, F8A389EC23342EB10010F405 /* ConstantsNightScout.swift */, F8A1586022EDB844007F5B5D /* ConstantsNotifications.swift */, + F8E51D5E2448E2E8001C9E5A /* ConstantsShareWithLoop.swift */, F8A1586222EDB86E007F5B5D /* ConstantsSounds.swift */, F8A1587022EDC865007F5B5D /* ConstantsSpeakReadingLanguages.swift */, F8A1586E22EDC7EE007F5B5D /* ConstantsSuspensionPrevention.swift */, - F8E3A2AA23DA520B00E5E98A /* ConstantsWatch.swift */, - F8E51D5E2448E2E8001C9E5A /* ConstantsShareWithLoop.swift */, F8AF36142455C6F700B5977B /* ConstantsTrace.swift */, + F8E3A2AA23DA520B00E5E98A /* ConstantsWatch.swift */, ); name = Constants; path = xdrip/Constants; @@ -2112,6 +2125,7 @@ F8EA6C8021B723A80082976B /* Utilities */ = { isa = PBXGroup; children = ( + F85FF38F25288860004E6FF1 /* HouseKeeping */, F8AF11F624B1E6D700AE5BA2 /* Errors */, F8C5EBE622F38F0E00563B5F /* Trace.swift */, F821CF69229FC22D005C1E43 /* Network */, @@ -3005,6 +3019,7 @@ F8F9722723A5915900C3F17D /* CGMBluconTransmitter.swift in Sources */, F8E3A2A923D906C200E5E98A /* WatchManager.swift in Sources */, F8B3A80A227A3D11004BA588 /* TextsAlertTypeSettings.swift in Sources */, + F85FF39125288870004E6FF1 /* HouseKeeper.swift in Sources */, F8F9721823A5915900C3F17D /* CGMBlueReaderTransmitter.swift in Sources */, F8177029248ED6E600AA3600 /* LibreRawGlucoseOOPData.swift in Sources */, F8252867243E50FE0067AF77 /* ConstantsLibre.swift in Sources */, @@ -3100,6 +3115,7 @@ F8A389EB233175A10010F405 /* SettingsViewM5StackSettingsViewModel.swift in Sources */, F816E1242439DB63009EE65B /* DexcomG4+BluetoothPeripheral.swift in Sources */, F8AF11F124B11FD400AE5BA2 /* LibreOOPWebError.swift in Sources */, + F8E5404C2522624800052CE5 /* ConstantsHousekeeping.swift in Sources */, F8AF120124B9082000AE5BA2 /* Calibration+NightScout.swift in Sources */, F808D2D2240329E80084B5DB /* Bubble+BluetoothPeripheral.swift in Sources */, F8025C1321DA683400ECF0C0 /* Data.swift in Sources */, diff --git a/xdrip/Constants/ConstantsHousekeeping.swift b/xdrip/Constants/ConstantsHousekeeping.swift new file mode 100644 index 00000000..c5f21f8d --- /dev/null +++ b/xdrip/Constants/ConstantsHousekeeping.swift @@ -0,0 +1,8 @@ +import Foundation + +enum ConstantsHousekeeping { + + /// how long to keep bgReadings + static let retentionPeriodBgReadingsInDays = 90.0 + +} diff --git a/xdrip/Constants/ConstantsLog.swift b/xdrip/Constants/ConstantsLog.swift index edfc0cc1..e72deb17 100644 --- a/xdrip/Constants/ConstantsLog.swift +++ b/xdrip/Constants/ConstantsLog.swift @@ -136,5 +136,8 @@ enum ConstantsLog { /// trace static let categoryTraceSettingsViewModel = "TraceSettingsViewModel" + /// housekeeping + static let categoryHouseKeeper = "HouseKeeper " + } diff --git a/xdrip/Core Data/accessors/BgReadingsAccessor.swift b/xdrip/Core Data/accessors/BgReadingsAccessor.swift index 900f5135..f4c3ec36 100644 --- a/xdrip/Core Data/accessors/BgReadingsAccessor.swift +++ b/xdrip/Core Data/accessors/BgReadingsAccessor.swift @@ -105,7 +105,7 @@ class BgReadingsAccessor { } } - /// gets readings on a managedObjectContact that is created with concurrencyType: .privateQueueConcurrencyType + /// gets readings on a managedObjectContext that is created with concurrencyType: .privateQueueConcurrencyType /// - returns: /// readings sorted by timestamp, ascending (ie first is oldest) /// - parameters: @@ -144,6 +144,20 @@ class BgReadingsAccessor { } + // deletes readings, to be used for readings retrieved with getBgReadingsOnPrivateManagedObjectContext, to be called on background thread + func deleteReadingOnPrivateManagedObjectContext(bgReading: BgReading) { + + privateManagedObjectContext.delete(bgReading) + + // save changes to coredata + do { + try self.privateManagedObjectContext.save() + } catch { + trace("in deleteReadingOnPrivateManagedObjectContext, Unable to Save Changes of Private Managed Object Context, error.localizedDescription = %{public}@", log: self.log, category: ConstantsLog.categoryApplicationDataBgReadings, type: .error, error.localizedDescription) + } + + } + // MARK: - private helper functions /// returnvalue can be empty array diff --git a/xdrip/Core Data/accessors/CalibrationsAccessor.swift b/xdrip/Core Data/accessors/CalibrationsAccessor.swift index 86d667f1..268cbce9 100644 --- a/xdrip/Core Data/accessors/CalibrationsAccessor.swift +++ b/xdrip/Core Data/accessors/CalibrationsAccessor.swift @@ -12,10 +12,18 @@ class CalibrationsAccessor { /// CoreDataManager to use private let coreDataManager:CoreDataManager + /// to be used when fetch request needs to run on a background thread + private let privateManagedObjectContext: NSManagedObjectContext + // MARK: - initializer init(coreDataManager:CoreDataManager) { + self.coreDataManager = coreDataManager + + privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + privateManagedObjectContext.persistentStoreCoordinator = coreDataManager.mainManagedObjectContext.persistentStoreCoordinator + } // MARK: - functions @@ -80,6 +88,60 @@ class CalibrationsAccessor { return calibrations } + /// gets calibrations on a managedObjectContext that is created with concurrencyType: .privateQueueConcurrencyType + /// - returns: + /// calibrations sorted by timestamp, ascending (ie first is oldest) + /// - parameters: + /// - to : if specified, only return calibrations with timestamp smaller than fromDate (not equal to) + /// - from : if specified, only return calibrations with timestamp greater than fromDate (not equal to) + func getCalibrationsOnPrivateManagedObjectContext(from: Date?, to: Date?) -> [Calibration] { + + let fetchRequest: NSFetchRequest = Calibration.fetchRequest() + fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(Calibration.timeStamp), ascending: true)] + + // create predicate + if let from = from, to == nil { + let predicate = NSPredicate(format: "timeStamp > %@", NSDate(timeIntervalSince1970: from.timeIntervalSince1970)) + fetchRequest.predicate = predicate + } else if let to = to, from == nil { + let predicate = NSPredicate(format: "timeStamp < %@", NSDate(timeIntervalSince1970: to.timeIntervalSince1970)) + fetchRequest.predicate = predicate + } else if let to = to, let from = from { + let predicate = NSPredicate(format: "timeStamp < %@ AND timeStamp > %@", NSDate(timeIntervalSince1970: to.timeIntervalSince1970), NSDate(timeIntervalSince1970: from.timeIntervalSince1970)) + fetchRequest.predicate = predicate + } + + var calibrations = [Calibration]() + + privateManagedObjectContext.performAndWait { + do { + // Execute Fetch Request + calibrations = try fetchRequest.execute() + } catch { + let fetchError = error as NSError + trace("in getCalibrationsOnPrivateManagedObjectContext, Unable to Execute Calibration Fetch Request : %{public}@", log: self.log, category: ConstantsLog.categoryApplicationDataCalibrations, type: .error, fetchError.localizedDescription) + } + } + + return calibrations + + } + + // deletes Calibration, to be used for Calibration retrieved with getCalibrationOnPrivateManagedObjectContext, to be called on background thread + func deleteCalibrationOnPrivateManagedObjectContext(calibration: Calibration) { + + privateManagedObjectContext.delete(calibration) + + // save changes to coredata + do { + try self.privateManagedObjectContext.save() + } catch { + trace("in deleteCalibrationOnPrivateManagedObjectContext, Unable to Save Changes of Private Managed Object Context, error.localizedDescription = %{public}@", log: self.log, category: ConstantsLog.categoryApplicationDataCalibrations, type: .error, error.localizedDescription) + } + + } + + // MARK: - private helper functions private func getFirstOrLastCalibration(withActivesensor sensor:Sensor, first:Bool) -> Calibration? { diff --git a/xdrip/Utilities/HouseKeeping/HouseKeeper.swift b/xdrip/Utilities/HouseKeeping/HouseKeeper.swift new file mode 100644 index 00000000..ab2456dc --- /dev/null +++ b/xdrip/Utilities/HouseKeeping/HouseKeeper.swift @@ -0,0 +1,101 @@ +import Foundation +import os + +/// housekeeping like remove old readings from coredata +class HouseKeeper { + + // MARK: - private properties + + /// for logging + private var log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryHouseKeeper) + + /// BgReadingsAccessor instance + private var bgReadingsAccessor:BgReadingsAccessor + + /// CalibrationsAccessor instance + private var calibrationsAccessor:CalibrationsAccessor + + // up to which date shall we delete old calibrations + private var toDate: Date + + // MARK: - intializer + + init(bgReadingsAccessor: BgReadingsAccessor, calibrationsAccessor: CalibrationsAccessor) { + + self.bgReadingsAccessor = bgReadingsAccessor + + self.calibrationsAccessor = calibrationsAccessor + + self.toDate = Date(timeIntervalSinceNow: -ConstantsHousekeeping.retentionPeriodBgReadingsInDays*24*3600) + + } + + // MARK: - public functions + + /// housekeeping activities to be done only once per app start up like delete old readings + public func doAppStartUpHouseKeeping() { + + DispatchQueue.global().async { + + // delete old readings + self.deleteOldReadings() + + // delete old calibrations + self.deleteOldCalibrations() + + } + } + + // MARK: - private functions + + /// deletes old readings. Readings older than ConstantsHousekeeping.retentionPeriodBgReadingsInDays will be deleted + private func deleteOldReadings() { + + // get old readings to delete + let oldReadings = self.bgReadingsAccessor.getBgReadingsOnPrivateManagedObjectContext(from: nil, to: self.toDate) + + if oldReadings.count > 0 { + + trace("in deleteOldReadings, number of bg readings to delete : %{public}@, to date = %{public}@", log: self.log, category: ConstantsLog.categoryHouseKeeper, type: .info, oldReadings.count.description, self.toDate.description(with: .current)) + + } + + // delete them + for oldReading in oldReadings { + + bgReadingsAccessor.deleteReadingOnPrivateManagedObjectContext(bgReading: oldReading) + + } + + } + + /// deletes old calibrations. Readings older than ConstantsHousekeeping.retentionPeriodBgReadingsInDays will be deleted + private func deleteOldCalibrations() { + + // get old calibrations to delete + let oldCalibrations = self.calibrationsAccessor.getCalibrationsOnPrivateManagedObjectContext(from: nil, to: self.toDate) + + if oldCalibrations.count > 0 { + + trace("in deleteOldCalibrations, number of calibrations candidate for deletion : %{public}@, to date = %{public}@", log: self.log, category: ConstantsLog.categoryHouseKeeper, type: .info, oldCalibrations.count.description, self.toDate.description(with: .current)) + + } + + // for each calibration that doesn't have any bg readings anymore, delete it + for oldCalibration in oldCalibrations { + + if (oldCalibration.bgreadings.count > 0 ) { + + trace("in deleteOldCalibrations, calibration with date %{public}@ will not be deleted beause there's still %{public}@ bgreadings", log: self.log, category: ConstantsLog.categoryHouseKeeper, type: .info, oldCalibration.timeStamp.description(with: .current), oldCalibration.bgreadings.count.description) + + } else { + + calibrationsAccessor.deleteCalibrationOnPrivateManagedObjectContext(calibration: oldCalibration) + + } + + } + + } + +} diff --git a/xdrip/View Controllers/Root View Controller/RootViewController.swift b/xdrip/View Controllers/Root View Controller/RootViewController.swift index 227a5fb3..82631eea 100644 --- a/xdrip/View Controllers/Root View Controller/RootViewController.swift +++ b/xdrip/View Controllers/Root View Controller/RootViewController.swift @@ -206,6 +206,9 @@ final class RootViewController: UIViewController { return dateFormatter }() + /// housekeeper instance + private var houseKeeper: HouseKeeper? + /// current value of webOPEnabled, if nil then it means no cgmTransmitter connected yet , false is used as value /// - used to detect changes in the value /// @@ -272,6 +275,9 @@ final class RootViewController: UIViewController { self.setupApplicationData() + // housekeeper should be non nil here, kall housekeeper + self.houseKeeper?.doAppStartUpHouseKeeping() + // glucoseChartManager still needs the reference to coreDataManager self.glucoseChartManager.coreDataManager = self.coreDataManager @@ -408,7 +414,7 @@ final class RootViewController: UIViewController { } } - // creates activeSensor, bgreadingsAccessor, calibrationsAccessor, NightScoutUploadManager, soundPlayer, dexcomShareUploadManager, nightScoutFollowManager, alertManager, healthKitManager, bgReadingSpeaker, bluetoothPeripheralManager, watchManager + // creates activeSensor, bgreadingsAccessor, calibrationsAccessor, NightScoutUploadManager, soundPlayer, dexcomShareUploadManager, nightScoutFollowManager, alertManager, healthKitManager, bgReadingSpeaker, bluetoothPeripheralManager, watchManager, housekeeper private func setupApplicationData() { // setup Trace @@ -430,6 +436,12 @@ final class RootViewController: UIViewController { // instantiate calibrations calibrationsAccessor = CalibrationsAccessor(coreDataManager: coreDataManager) + guard let calibrationsAccessor = calibrationsAccessor else { + fatalError("In setupApplicationData, failed to initialize calibrationsAccessor") + } + + // instanstiate Housekeeper + houseKeeper = HouseKeeper(bgReadingsAccessor: bgReadingsAccessor, calibrationsAccessor: calibrationsAccessor) // setup nightscout synchronizer nightScoutUploadManager = NightScoutUploadManager(coreDataManager: coreDataManager, messageHandler: { (title:String, message:String) in