diff --git a/Prisma.xcodeproj/project.pbxproj b/Prisma.xcodeproj/project.pbxproj index 68aa227..1042ad6 100644 --- a/Prisma.xcodeproj/project.pbxproj +++ b/Prisma.xcodeproj/project.pbxproj @@ -72,9 +72,11 @@ A9D83F962B083794000D0C78 /* SpeziFirebaseAccountStorage in Frameworks */ = {isa = PBXBuildFile; productRef = A9D83F952B083794000D0C78 /* SpeziFirebaseAccountStorage */; }; A9DFE8A92ABE551400428242 /* AccountButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DFE8A82ABE551400428242 /* AccountButton.swift */; }; A9FE7AD02AA39BAB0077B045 /* AccountSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FE7ACF2AA39BAB0077B045 /* AccountSheet.swift */; }; - E4C766262B72D50500C1DEDA /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C766252B72D50500C1DEDA /* WebView.swift */; }; - AC69903E2B6C5A2F00D92970 /* PrivacyControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC69903D2B6C5A2F00D92970 /* PrivacyControls.swift */; }; + AC69903E2B6C5A2F00D92970 /* PrivacyModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC69903D2B6C5A2F00D92970 /* PrivacyModule.swift */; }; AC6990402B6C627100D92970 /* ToggleTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC69903F2B6C627100D92970 /* ToggleTestView.swift */; }; + D8027E912B90655700BB9466 /* ManageDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8027E902B90655700BB9466 /* ManageDataView.swift */; }; + D8F136C52B85CEED000BA7AE /* DeleteDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F136C42B85CEED000BA7AE /* DeleteDataView.swift */; }; + E4C766262B72D50500C1DEDA /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C766252B72D50500C1DEDA /* WebView.swift */; }; F8AF6F9A2B5F2B1A0011C32D /* AppIcon-NoBG.png in Resources */ = {isa = PBXBuildFile; fileRef = F8AF6F992B5F2B1A0011C32D /* AppIcon-NoBG.png */; }; F8AF6F9F2B5F35400011C32D /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AF6F9E2B5F35400011C32D /* ChatView.swift */; }; F8AF6FA52B5F3AE70011C32D /* EventContextCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AF6FA42B5F3AE70011C32D /* EventContextCard.swift */; }; @@ -152,9 +154,11 @@ A9720E422ABB68CC00872D23 /* AccountSetupHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSetupHeader.swift; sourceTree = ""; }; A9DFE8A82ABE551400428242 /* AccountButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountButton.swift; sourceTree = ""; }; A9FE7ACF2AA39BAB0077B045 /* AccountSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSheet.swift; sourceTree = ""; }; - E4C766252B72D50500C1DEDA /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; - AC69903D2B6C5A2F00D92970 /* PrivacyControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyControls.swift; sourceTree = ""; }; + AC69903D2B6C5A2F00D92970 /* PrivacyModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyModule.swift; sourceTree = ""; }; AC69903F2B6C627100D92970 /* ToggleTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleTestView.swift; sourceTree = ""; }; + D8027E902B90655700BB9466 /* ManageDataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageDataView.swift; sourceTree = ""; }; + D8F136C42B85CEED000BA7AE /* DeleteDataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteDataView.swift; sourceTree = ""; }; + E4C766252B72D50500C1DEDA /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; F8AF6F992B5F2B1A0011C32D /* AppIcon-NoBG.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIcon-NoBG.png"; sourceTree = ""; }; F8AF6F9E2B5F35400011C32D /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = ""; }; F8AF6FA42B5F3AE70011C32D /* EventContextCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventContextCard.swift; sourceTree = ""; }; @@ -409,8 +413,10 @@ AC4A1ED12B69D91D0095D1AE /* PrivacyControls */ = { isa = PBXGroup; children = ( - AC69903D2B6C5A2F00D92970 /* PrivacyControls.swift */, + AC69903D2B6C5A2F00D92970 /* PrivacyModule.swift */, AC69903F2B6C627100D92970 /* ToggleTestView.swift */, + D8F136C42B85CEED000BA7AE /* DeleteDataView.swift */, + D8027E902B90655700BB9466 /* ManageDataView.swift */, ); path = PrivacyControls; sourceTree = ""; @@ -673,6 +679,7 @@ 2FE5DC4029EDD7EE004B9AB4 /* FeatureFlags.swift in Sources */, F8AF6FA52B5F3AE70011C32D /* EventContextCard.swift in Sources */, 2FE5DC4629EDD7F2004B9AB4 /* Bundle+Image.swift in Sources */, + D8F136C52B85CEED000BA7AE /* DeleteDataView.swift in Sources */, F8AF6FB92B5F72650011C32D /* PrismaStandard+HealthKit.swift in Sources */, 2FE5DC4F29EDD7FA004B9AB4 /* EventContext.swift in Sources */, 2FE5DC5029EDD7FA004B9AB4 /* EventContextView.swift in Sources */, @@ -689,9 +696,10 @@ 2F5E32BD297E05EA003432F8 /* PrismaDelegate.swift in Sources */, 2FE5DC5229EDD7FA004B9AB4 /* PrismaScheduler.swift in Sources */, A9FE7AD02AA39BAB0077B045 /* AccountSheet.swift in Sources */, + D8027E912B90655700BB9466 /* ManageDataView.swift in Sources */, F8AF6FB42B5F6EDC0011C32D /* PrismaModule.swift in Sources */, AC6990402B6C627100D92970 /* ToggleTestView.swift in Sources */, - AC69903E2B6C5A2F00D92970 /* PrivacyControls.swift in Sources */, + AC69903E2B6C5A2F00D92970 /* PrivacyModule.swift in Sources */, 653A2551283387FE005D4D48 /* Prisma.swift in Sources */, 2FE5DC3629EDD7CA004B9AB4 /* HealthKitPermissions.swift in Sources */, 5661552E2AB854C000209B80 /* PackageHelper.swift in Sources */, diff --git a/Prisma/Home.swift b/Prisma/Home.swift index 3302616..ffda150 100644 --- a/Prisma/Home.swift +++ b/Prisma/Home.swift @@ -17,6 +17,7 @@ struct HomeView: View { case chat case contact case mockUpload + case privacy } static var accountEnabled: Bool { @@ -45,6 +46,11 @@ struct HomeView: View { .tabItem { Label("CONTACTS_TAB_TITLE", systemImage: "person.fill") } + ManageDataView() + .tag(Tabs.privacy) + .tabItem { + Label("PRIVACY_CONTROLS_TITLE", systemImage: "gear") + } if FeatureFlags.disableFirebase { MockUpload(presentingAccount: $presentingAccount) .tag(Tabs.mockUpload) diff --git a/Prisma/PrismaDelegate.swift b/Prisma/PrismaDelegate.swift index 1a8d4c5..4ed5b5e 100644 --- a/Prisma/PrismaDelegate.swift +++ b/Prisma/PrismaDelegate.swift @@ -21,6 +21,31 @@ import SwiftUI class PrismaDelegate: SpeziAppDelegate { + private let sampleList = [ + // Activity + HKQuantityType(.stepCount), + HKQuantityType(.distanceWalkingRunning), + HKQuantityType(.basalEnergyBurned), + HKQuantityType(.activeEnergyBurned), + HKQuantityType(.flightsClimbed), + HKQuantityType(.appleExerciseTime), + HKQuantityType(.appleMoveTime), + HKQuantityType(.appleStandTime), + + // Vital Signs + HKQuantityType(.heartRate), + HKQuantityType(.restingHeartRate), + HKQuantityType(.heartRateVariabilitySDNN), + HKQuantityType(.walkingHeartRateAverage), + HKQuantityType(.oxygenSaturation), + HKQuantityType(.respiratoryRate), + HKQuantityType(.bodyTemperature), + + // Other events + HKCategoryType(.sleepAnalysis), + HKWorkoutType.workoutType() + ] + override var configuration: Configuration { Configuration(standard: PrismaStandard()) { if !FeatureFlags.disableFirebase { @@ -53,6 +78,7 @@ class PrismaDelegate: SpeziAppDelegate { PrismaScheduler() OnboardingDataSource() PrismaPushNotifications() + PrivacyModule(sampleTypeList: sampleList) } } @@ -70,21 +96,11 @@ class PrismaDelegate: SpeziAppDelegate { ) } - private var healthKit: HealthKit { HealthKit { CollectSamples( - [ - HKQuantityType(.activeEnergyBurned), - HKQuantityType(.stepCount), - HKQuantityType(.distanceWalkingRunning), - HKQuantityType(.vo2Max), - HKQuantityType(.heartRate), - HKQuantityType(.restingHeartRate), - HKQuantityType(.oxygenSaturation), - HKQuantityType(.respiratoryRate), - HKQuantityType(.walkingHeartRateAverage) - ], + // https://developer.apple.com/documentation/healthkit/data_types#2939032 + Set(sampleList), /// predicate to request data from one month in the past to present. predicate: HKQuery.predicateForSamples( withStart: Calendar.current.date(byAdding: .month, value: -1, to: .now), diff --git a/Prisma/PrivacyControls/DeleteDataView.swift b/Prisma/PrivacyControls/DeleteDataView.swift new file mode 100644 index 0000000..c971d38 --- /dev/null +++ b/Prisma/PrivacyControls/DeleteDataView.swift @@ -0,0 +1,85 @@ +// +// This source file is part of the Stanford Prisma Application based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +// +// DeleteDataView.swift +// Prisma +// +// Created by Evelyn Hur, Caroline Tran on 2/20/24. +// + +import FirebaseFirestore +import Foundation +import Spezi +import SpeziHealthKit +import SwiftUI + + +struct DeleteDataView: View { + @Environment(PrivacyModule.self) private var privacyModule + @Environment(PrismaStandard.self) private var standard + // category identifier is passed into DeleteDataView from ManageDataView + var categoryIdentifier: String + + // NEXT STEPS: timeArrayStatic will be replaced by timestampsArray which is read in from firestore using the categoryIdentifier and getPath + @State private var timeArrayStatic = ["2023-11-14T20:39:44.467", "2023-11-14T20:41:00.000", "2023-11-14T20:42:00.000"] + // var timeArray = getLastTimestamps(quantityType: "stepcount") + + var body: some View { + // create a list of all the time stamps for this category + // get rid of spacing once we insert custom time range + VStack(spacing: -400) { + Form { + Section(header: Text("Allow to Read")) { + Toggle(self.privacyModule.identifierUIString[self.categoryIdentifier] ?? "Cannot Find Data Type", isOn: Binding( + get: { + // Return the current value or a default value if the key does not exist + self.privacyModule.togglesMap[self.categoryIdentifier] ?? false + }, + set: { newValue in + // Update the dictionary with the new value + self.privacyModule.togglesMap[self.categoryIdentifier] = newValue + } + )) + } + } + NavigationView { + // Toggle corresponding to the proper data to exclude all data of this type + List { + Section(header: Text("Delete by time")) { + ForEach(timeArrayStatic, id: \.self) { timestamp in + Text(timestamp) + } + // on delete, remove it on the UI and set flag in firebase + .onDelete { indices in + let timestampsToDelete = indices.map { timeArrayStatic[$0] } + deleteInBackend(identifier: categoryIdentifier, timestamps: timestampsToDelete) + timeArrayStatic.remove(atOffsets: indices) + } + } + } + .padding(.top, -40) + .navigationBarItems(trailing: EditButton()) + } + } + .navigationTitle(privacyModule.identifierUIString[categoryIdentifier] ?? "Identifier Title Not Found") + } + + func deleteInBackend(identifier: String, timestamps: [String]) { + for timestamp in timestamps { + Task { + await standard.addDeleteFlag(selectedTypeIdentifier: identifier, timestamp: timestamp) + } + } + } +} + + +#Preview { + DeleteDataView(categoryIdentifier: "Example Preview: DeleteDataView") +} diff --git a/Prisma/PrivacyControls/ManageDataView.swift b/Prisma/PrivacyControls/ManageDataView.swift new file mode 100644 index 0000000..bdde07f --- /dev/null +++ b/Prisma/PrivacyControls/ManageDataView.swift @@ -0,0 +1,47 @@ +// +// This source file is part of the Stanford Prisma Application based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +// +// ManageDataView.swift +// Prisma +// +// Created by Evelyn Hur on 2/28/24. +// + +import SwiftUI + +struct ManageDataView: View { + @Environment(PrivacyModule.self) private var privacyModule + var body: some View { + NavigationView { + List(privacyModule.dataCategoryItems, id: \.name) { item in + NavigationLink(destination: DeleteDataView(categoryIdentifier: item.name)) { + HStack(alignment: .center, spacing: 10) { + Image(systemName: item.iconName) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 35, height: 35) + .accessibility(label: Text("accessibility text temp")) + VStack(alignment: .leading, spacing: 4) { + Text(privacyModule.identifierUIString[item.name] ?? "Identifier UI String Not Found") + .font(.headline) + Text(item.enabledStatus) + .font(.subheadline) + .foregroundColor(.gray) + } + } + } + } + .navigationTitle("Manage Data") + } + } +} + +#Preview { + ManageDataView() +} diff --git a/Prisma/PrivacyControls/PrivacyControls.swift b/Prisma/PrivacyControls/PrivacyControls.swift deleted file mode 100644 index c0ea206..0000000 --- a/Prisma/PrivacyControls/PrivacyControls.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// PrivacyControls.swift -// Prisma -// -// Created by Dhruv Naik on 2/1/24. - -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT - -import Foundation -import Spezi -import SwiftUI - - -@Observable -public class PrivacyModule: DefaultInitializable, EnvironmentAccessible { - var configuration: Configuration { - Configuration(standard: PrismaStandard()) { } - } - var includeStepCountUpload = false - var includeActiveEnergyBurned = true - var includeDistanceWalkingRunning = true - var includeVo2Max = true - var includeHeartRate = true - var includeRestingHeartRate = true - var includeOxygenSaturation = true - var includeRespiratoryRate = true - var includeWalkingHRAverage = true - - public required init() {} - - public func getCurrentToggles() -> [String: Bool] { - [ - "includeStepCountUpload": includeStepCountUpload, - "includeActiveEnergyBurned": includeActiveEnergyBurned, - "includeDistanceWalkingRunning": includeDistanceWalkingRunning, - "includeVo2Max": includeVo2Max, - "includeHeartRate": includeHeartRate, - "includeRestingHeartRate": includeRestingHeartRate, - "includeOxygenSaturation": includeOxygenSaturation, - "includeRespiratoryRate": includeRespiratoryRate, - "includeWalkingHRAverage": includeWalkingHRAverage - ] - } -} diff --git a/Prisma/PrivacyControls/PrivacyModule.swift b/Prisma/PrivacyControls/PrivacyModule.swift new file mode 100644 index 0000000..bc4ccb6 --- /dev/null +++ b/Prisma/PrivacyControls/PrivacyModule.swift @@ -0,0 +1,115 @@ +// +// This source file is part of the Stanford Prisma Application based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + + +// PrivacyControls.swift +// Prisma +// +// Created by Dhruv Naik on 2/1/24. +// Edited by Evelyn Hur and Caroline on 2/28/24. + +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT + +import Foundation +import HealthKit +import Spezi +import SwiftUI + + +public class PrivacyModule: Module, EnvironmentAccessible { + public struct DataCategoryItem { + var name: String + var iconName: String + var enabledStatus: String + } + + public var iconsMapping: [String: String] = [ + "activeenergyburned": "flame", + "distancewalkingrunning": "figure.walk", + "heartrate": "waveform.path.ecg", + "oxygensaturation": "drop.fill", + "respiratoryrate": "lungs.fill", + "restingheartrate": "arrow.down.heart.fill", + "stepcount": "shoeprints.fill", + "vo2max": "wind", + "walkingheartrateaverage": "figure.step.training" + ] + + public var togglesMap: [String: Bool] = [ + "activeenergyburned": true, + "distancewalkingrunning": true, + "heartrate": true, + "oxygensaturation": true, + "respiratoryrate": true, + "restingheartrate": true, + "stepcount": true, + "vo2max": true, + "walkingheartrateaverage": true + ] + + public var identifierUIString: [String: String] = [ + "activeenergyburned": "Active Energy Burned", + "distancewalkingrunning": "Distance Walking Running", + "heartrate": "Heart Rate", + "oxygensaturation": "Oxygen Saturation", + "respiratoryrate": "Respiratory Rate", + "restingheartrate": "Resting Heart Rate", + "stepcount": "Step Count", + "vo2max": "VO2 Max", + "walkingheartrateaverage": "Walking Heart Rate Average" + ] + + var dataCategoryItems: [DataCategoryItem] = [] + var sampleTypeList: [HKSampleType] + var toggleMapUpdated: [String: Bool] = [:] + + @StandardActor var standard: PrismaStandard + + + var configuration: Configuration { + Configuration(standard: PrismaStandard()) { } + } + + public required init(sampleTypeList: [HKSampleType]) { + self.sampleTypeList = sampleTypeList + self.dataCategoryItems = self.getDataCategoryItems() + } + + public func configure() { + Task { + toggleMapUpdated = await getHKSampleTypeMappings() + } + } + + public func getDataCategoryItems() -> [DataCategoryItem] { + // make dictionary into alphabetically sorted array of key-value tuples + let sortedDataCategoryItems = identifierUIString.sorted { $0.key < $1.key } + for dataCategoryPair in sortedDataCategoryItems { + dataCategoryItems.append( + DataCategoryItem( + name: dataCategoryPair.0, + iconName: (iconsMapping[dataCategoryPair.0] ?? "unable to get icon string"), + enabledStatus: (togglesMap[dataCategoryPair.0] ?? true) ? "Enabled" : "Disabled" + ) + ) + } + return dataCategoryItems + } + + public func getHKSampleTypeMappings() async -> [String: Bool] { + var toggleMapUpdated: [String: Bool] = [:] + + for sampleType in sampleTypeList { + let identifier = await standard.getSampleIdentifierFromHKSampleType(sampleType: sampleType) + toggleMapUpdated[identifier ?? "Unidentified Sample Type"] = true + } + return toggleMapUpdated + } +} diff --git a/Prisma/PrivacyControls/ToggleTestView.swift b/Prisma/PrivacyControls/ToggleTestView.swift index 62a7a84..7088245 100644 --- a/Prisma/PrivacyControls/ToggleTestView.swift +++ b/Prisma/PrivacyControls/ToggleTestView.swift @@ -13,23 +13,23 @@ import SwiftUI struct ToggleTestView: View { - @Bindable var privacyModule = PrivacyModule() + // @Bindable var privacyModule = PrivacyModule() var body: some View { NavigationView { - Form { - Toggle("Include Step Count Upload", isOn: $privacyModule.includeStepCountUpload) - Toggle("Include Active Energy Burned", isOn: $privacyModule.includeActiveEnergyBurned) - Toggle("Include Distance Walking Running", isOn: $privacyModule.includeDistanceWalkingRunning) - Toggle("Include Vo2 Max", isOn: $privacyModule.includeVo2Max) - Toggle("Include Heart Rate", isOn: $privacyModule.includeHeartRate) - Toggle("Include Resting Heart Rate", isOn: $privacyModule.includeRestingHeartRate) - Toggle("Include Oxygen Saturation", isOn: $privacyModule.includeOxygenSaturation) - Toggle("Include Respiratory Rate", isOn: $privacyModule.includeRespiratoryRate) - Toggle("Include Walking Heart Rate Average", isOn: $privacyModule.includeWalkingHRAverage) - } - .navigationBarTitle("Privacy Settings") +// Form { +// Toggle("Include Step Count Upload", isOn: $privacyModule.includeStepCountUpload) +// Toggle("Include Active Energy Burned", isOn: $privacyModule.includeActiveEnergyBurned) +// Toggle("Include Distance Walking Running", isOn: $privacyModule.includeDistanceWalkingRunning) +// Toggle("Include Vo2 Max", isOn: $privacyModule.includeVo2Max) +// Toggle("Include Heart Rate", isOn: $privacyModule.includeHeartRate) +// Toggle("Include Resting Heart Rate", isOn: $privacyModule.includeRestingHeartRate) +// Toggle("Include Oxygen Saturation", isOn: $privacyModule.includeOxygenSaturation) +// Toggle("Include Respiratory Rate", isOn: $privacyModule.includeRespiratoryRate) +// Toggle("Include Walking Heart Rate Average", isOn: $privacyModule.includeWalkingHRAverage) +// } +// .navigationBarTitle("Privacy Settings") } } } diff --git a/Prisma/Resources/Localizable.xcstrings b/Prisma/Resources/Localizable.xcstrings index a3fa31d..d077cd6 100644 --- a/Prisma/Resources/Localizable.xcstrings +++ b/Prisma/Resources/Localizable.xcstrings @@ -3,6 +3,9 @@ "strings" : { "" : { + }, + "accessibility text temp" : { + }, "ACCOUNT_NEXT" : { "localizations" : { @@ -76,7 +79,7 @@ } } }, - "chat" : { + "Allow to Read" : { }, "Chat" : { @@ -152,6 +155,9 @@ }, "Currently there are no surveys available, you will be notified about upcoming surveys." : { + }, + "Delete by time" : { + }, "EMMA_BRUNSKILL_BIO" : { @@ -314,32 +320,7 @@ } } }, - "Invalid URL" : {}, - "Include Active Energy Burned" : { - - }, - "Include Distance Walking Running" : { - - }, - "Include Heart Rate" : { - - }, - "Include Oxygen Saturation" : { - - }, - "Include Respiratory Rate" : { - - }, - "Include Resting Heart Rate" : { - - }, - "Include Step Count Upload" : { - - }, - "Include Vo2 Max" : { - - }, - "Include Walking Heart Rate Average" : { + "Invalid URL" : { }, "JAMES_LANDAY_BIO" : { @@ -361,6 +342,9 @@ } } } + }, + "Manage Data" : { + }, "MATTHEW_JOERKE_BIO" : { @@ -468,8 +452,17 @@ } } }, - "Privacy Settings" : { - + "PRIVACY_CONTROLS_TITLE" : { + "comment" : "MARK: - Privacy Controls", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacy Controls" + } + } + } }, "PROJECT_LICENSE_DESCRIPTION" : { "localizations" : { diff --git a/Prisma/Standard/PrismaStandard+Extension.swift b/Prisma/Standard/PrismaStandard+Extension.swift index 03966c8..29a5a73 100644 --- a/Prisma/Standard/PrismaStandard+Extension.swift +++ b/Prisma/Standard/PrismaStandard+Extension.swift @@ -13,12 +13,16 @@ extension String { /// converts a HKSample Type string representation to a lower cased id. /// e.g. "HKQuantityTypeIdentifierStepCount" => "stepcount". var healthKitDescription: String { - let description = self - if description.hasPrefix("HKQuantityTypeIdentifier") { - // removes the HKQuantityTypeIdentifier prefix. - return description.dropFirst(24).lowercased() + if self == "workout" { + return "workout" } - return "unknown" + + let prefixes = ["HKQuantityTypeIdentifier", "HKCategoryTypeIdentifier", "HKCorrelationTypeIdentifier", "HKWorkoutTypeIdentifier"] + for prefix in prefixes where self.hasPrefix(prefix) { + return self.dropFirst(prefix.count).lowercased() + } + // return "unknown" + return self } } diff --git a/Prisma/Standard/PrismaStandard+HealthKit.swift b/Prisma/Standard/PrismaStandard+HealthKit.swift index e584294..016e5b9 100644 --- a/Prisma/Standard/PrismaStandard+HealthKit.swift +++ b/Prisma/Standard/PrismaStandard+HealthKit.swift @@ -32,44 +32,82 @@ import SpeziHealthKit extension PrismaStandard { + func getSampleIdentifier(sample: HKSample) -> String? { + switch sample { + case let quantitySample as HKQuantitySample: + return quantitySample.quantityType.identifier + case let categorySample as HKCategorySample: + return categorySample.categoryType.identifier + case is HKWorkout: + // return "\lcal(workout.workoutActivityType)" + return "workout" + // Add more cases for other HKSample subclasses if needed + default: + return nil + } + } + + /// Takes in HKSampleType and returns the corresponding identifier string + /// + /// - Parameters: + /// - sampleType: HKSampleType to find identifier for + /// - Returns: A string for the sample type identifier. + public func getSampleIdentifierFromHKSampleType(sampleType: HKSampleType) -> String? { + if let quantityType = sampleType as? HKQuantityType { + return quantityType.identifier + } else if let categoryType = sampleType as? HKCategoryType { + return categoryType.identifier + } else if sampleType is HKWorkoutType { + return "workout" + } + // Default case for other HKSampleTypes + else { + return "Unknown Sample Type" + } + } + /// Adds a new `HKSample` to the Firestore. /// - Parameter response: The `HKSample` that should be added. func add(sample: HKSample) async { - guard let quantityType = sample.sampleType as? HKQuantityType else { - return - } - - var sampleToToggleNameMapping: [HKQuantityType?: String] = [ - HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned): "includeActiveEnergyBurned", - HKQuantityType.quantityType(forIdentifier: .stepCount): "includeStepCountUpload", - HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning): "includeDistanceWalkingRunning", - HKQuantityType.quantityType(forIdentifier: .vo2Max): "includeVo2Max", - HKQuantityType.quantityType(forIdentifier: .heartRate): "includeHeartRate", - HKQuantityType.quantityType(forIdentifier: .restingHeartRate): "includeRestingHeartRate", - HKQuantityType.quantityType(forIdentifier: .oxygenSaturation): "includeOxygenSaturation", - HKQuantityType.quantityType(forIdentifier: .respiratoryRate): "includeRespiratoryRate", - HKQuantityType.quantityType(forIdentifier: .walkingHeartRateAverage): "includeWalkingHeartRateAverage" - ] - var toggleNameToBoolMapping: [String: Bool] = PrivacyModule().getCurrentToggles() - - if let variableName = sampleToToggleNameMapping[quantityType] { - let response: Bool = toggleNameToBoolMapping[variableName] ?? false - - if !response { - return - } + let identifier: String + if let id = getSampleIdentifier(sample: sample) { + print("Sample identifier: \(id)") + identifier = id } else { + print("Unknown sample type") return } - let path: String +// var sampleToToggleNameMapping: [HKQuantityType?: String] = [ +// HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned): "includeActiveEnergyBurned", +// HKQuantityType.quantityType(forIdentifier: .stepCount): "includeStepCountUpload", +// HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning): "includeDistanceWalkingRunning", +// HKQuantityType.quantityType(forIdentifier: .vo2Max): "includeVo2Max", +// HKQuantityType.quantityType(forIdentifier: .heartRate): "includeHeartRate", +// HKQuantityType.quantityType(forIdentifier: .restingHeartRate): "includeRestingHeartRate", +// HKQuantityType.quantityType(forIdentifier: .oxygenSaturation): "includeOxygenSaturation", +// HKQuantityType.quantityType(forIdentifier: .respiratoryRate): "includeRespiratoryRate", +// HKQuantityType.quantityType(forIdentifier: .walkingHeartRateAverage): "includeWalkingHeartRateAverage" +// ] +// var toggleNameToBoolMapping: [String: Bool] = PrivacyModule().getCurrentToggles() +// +// if let variableName = sampleToToggleNameMapping[quantityType] { +// let response: Bool = toggleNameToBoolMapping[variableName] ?? false +// +// if !response { +// return +// } +// } else { +// return +// } - // retrieve id of HKSample (e.g. HKQuantityTypeIdentifierStepCount) - let identifier = quantityType.identifier // convert the startDate of the HKSample to local time - let effectiveTimestamp = sample.startDate.localISOFormat() + let startDatetime = sample.startDate + let effectiveTimestamp = startDatetime.localISOFormat() + let endDatetime = sample.endDate.localISOFormat() + let path: String // path = HEALTH_KIT_PATH/raw/YYYY-MM-DDThh:mm:ss.mss do { path = try await getPath(module: .health(identifier)) + "raw/\(effectiveTimestamp)" @@ -100,4 +138,29 @@ extension PrismaStandard { } func remove(sample: HKDeletedObject) async { } + + func addDeleteFlag(selectedTypeIdentifier: String, timestamp: String) async { + let path: String + + do { + // call getPath to get the path for this user, up until this specific quantityType + path = try await getPath(module: .health(selectedTypeIdentifier)) + "raw/\(timestamp)" + print("selectedindentifier:" + selectedTypeIdentifier) + print("PATH FROM GET PATH: " + path) + } catch { + print("Failed to define path: \(error.localizedDescription)") + return + } + + // try push to Firestore. + do { + // add another key-value pair field for the delete flag + // merge new key-value with pre-existing data instead of overwriting it + let newData = ["deleteFlag": "true"] + try await Firestore.firestore().document(path).setData(newData, merge: true) + print("Successfully set deleteFlag to true.") + } catch { + print("Failed to set data in Firestore: \(error.localizedDescription)") + } + } } diff --git a/Prisma/Standard/PrismaStandard.swift b/Prisma/Standard/PrismaStandard.swift index 0f593db..0d1d6c5 100644 --- a/Prisma/Standard/PrismaStandard.swift +++ b/Prisma/Standard/PrismaStandard.swift @@ -93,11 +93,11 @@ actor PrismaStandard: Standard, EnvironmentAccessible, HealthKitConstraint, Onbo // HealthKit observations moduleText = "\(module.description)/\(type.healthKitDescription)" } - + print("moduleText:" + moduleText) // studies/STUDY_ID/users/USER_ID/MODULE_NAME/SUB_TYPE/... return "studies/\(PrismaStandard.STUDYID)/users/\(accountId)/\(moduleText)/" } - + func deletedAccount() async throws { // delete all user associated data do {