Skip to content

Commit

Permalink
Features/deletedata: Create Privacy Controls UI and Add deleteFlag to…
Browse files Browse the repository at this point in the history
… specified timestamp (#33)

# *Add deleteFlag to a specified timestamp in Firestore*

## ♻️ Current situation & Problem
While the current version of the app allows reading in health data to be
disabled via toggles in ToggleTestView.swift, it does not allow past
health data to be reviewed and hidden or deleted. This PR creates a page
ManageDataView for users to be able to see all health data types (e.g,
step count) with toggle status shown as well as detailed pages per
datatype in DeleteDataView that allows users to switch toggle status
(enable/disable reading data type) and hide data from a list of
timestamps. A function addDeleteFlag was created to add a deleteFlag and
set it to true for a timestamp the user chooses to hide.


## ⚙️ Release Notes 
Updates
- Created ManageDataView: A list of all healthkit data sample types that
link to a DeleteDataView screen specific to the selected category.
- Created new file DeleteDataView: Using the dictionaries in
PrivacyModule we created a details screen showing the disable/enable
status and a hardcoded array of timestamps (of collected data points).
This array will be replaced by an array populated by the actual
timestamps from firestore in the next PR. The user will navigate to this
screen to hide/unhide collected data.
- Inside Privacy Module: Created dictionaries iconsMapping, togglesMap,
and identifierUIString to map data sample types to icon image, toggle
status, and UI title respectively.
- Inside PrismaStandard+HealthKit.swift, created func
addDeleteFlag(quantityType: String, timestamp: String) async that takes
in a quantityType (e.g, stepcount, heartrate) and timestamp, uses
getPath, and uses setData to add and set a new flag “deleteFlag” to true
without replacing original fields.

Next Steps
- While the timestamps in the listview UI were hard-coded, in the next
few days, we will query the 10 most recent timestamps from firestore
using order and limit data, store it into an array, and display them
(allowing the user to also load in more sets of timestamps in increments
of 10).
- Change "delete" to "hide" data for adding deleteFlag
- Allow users to delete data view custom range (they will input start
and end date for deletion).


## 📚 Documentation
N/A


## ✅ Testing
On HCI-Prisma repository, run `firebase emulators:start
--import=firestore_export` to run the firebase emulator. Erase all
contents and settings from the device on the simulator. Log in with
“[[email protected]](mailto:[email protected])” and “testing123”. If a
timestamp is deleted on DeleteDataView.swift, a new field “deleteFlag”
set to true should be created inside the matching timestamp document in
the emulator.

## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/CS342/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/CS342/.github/blob/main/CONTRIBUTING.md):
- I agree to follow the [Code of
Conduct](https://github.com/CS342/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/CS342/.github/blob/main/CONTRIBUTING.md).

## Roles:

- Evelyn: We both worked together on each step but I focused on creating
the ManageDataView page to display a list of data type sources and
disabled/enabled status using a new struct DataCategoryItem; creating
environment accessible dictionaries in Privacy Modules to map data
sample types to toggle status, icon, and title; UI for displaying list
and animating deletion of timestamps in DeleteDataView; creating
addDeleteFlag to create and set new field deleteFlag to true in
corresponding timestamp in Firestore; and debugging addDeleteFlag to fix
out-of-index and incorrect getPath errors.
- Caroline: We both worked together on each step but I focused on the
linking between the screens (Home and navigation bar, ManageDataView to
DeleteDataView, communication between functions and dictionaries in
Privacy Modules); creating the user action trigger that writes a
deleteFlag into Firestore; restructuring our logic and functionality in
DeleteDataView to use getPath() and HKSamples; bug fixes for the UI on
both ManageDataView and DeleteDataView; and bug fixes to the
addDeleteFlag and UI item remove animation so the item is flagged in the
backend before the deletion is shown to the user.
- Dhruv: I focused on implementing certain functionalities throughout
the entire UI development process. This included providing the initial
architecture for the PrivacyModule class, adding toggle functionality
for a users to omit a certain data type from being collected within our
new UI post-midterm, and writing functions to set up this toggle
creation by altering the PrismaStandard class and the PrivacyModule
class to take in a list of HKSampleTypes and create a Binding set of
Toggles which map identifiers to the Boolean toggle’s state.

---------

Co-authored-by: Evelyn <[email protected]>
Co-authored-by: Bryant Jimenez <[email protected]>
Co-authored-by: Matthew Jörke <[email protected]>
Co-authored-by: dhruvna1k <[email protected]>
  • Loading branch information
5 people authored Mar 6, 2024
1 parent b4e57f9 commit 2873e1e
Show file tree
Hide file tree
Showing 12 changed files with 431 additions and 140 deletions.
20 changes: 14 additions & 6 deletions Prisma.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -152,9 +154,11 @@
A9720E422ABB68CC00872D23 /* AccountSetupHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSetupHeader.swift; sourceTree = "<group>"; };
A9DFE8A82ABE551400428242 /* AccountButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountButton.swift; sourceTree = "<group>"; };
A9FE7ACF2AA39BAB0077B045 /* AccountSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSheet.swift; sourceTree = "<group>"; };
E4C766252B72D50500C1DEDA /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = "<group>"; };
AC69903D2B6C5A2F00D92970 /* PrivacyControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyControls.swift; sourceTree = "<group>"; };
AC69903D2B6C5A2F00D92970 /* PrivacyModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyModule.swift; sourceTree = "<group>"; };
AC69903F2B6C627100D92970 /* ToggleTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleTestView.swift; sourceTree = "<group>"; };
D8027E902B90655700BB9466 /* ManageDataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageDataView.swift; sourceTree = "<group>"; };
D8F136C42B85CEED000BA7AE /* DeleteDataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteDataView.swift; sourceTree = "<group>"; };
E4C766252B72D50500C1DEDA /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = "<group>"; };
F8AF6F992B5F2B1A0011C32D /* AppIcon-NoBG.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIcon-NoBG.png"; sourceTree = "<group>"; };
F8AF6F9E2B5F35400011C32D /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
F8AF6FA42B5F3AE70011C32D /* EventContextCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventContextCard.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 = "<group>";
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand Down
6 changes: 6 additions & 0 deletions Prisma/Home.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ struct HomeView: View {
case chat
case contact
case mockUpload
case privacy
}

static var accountEnabled: Bool {
Expand Down Expand Up @@ -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)
Expand Down
40 changes: 28 additions & 12 deletions Prisma/PrismaDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -53,6 +78,7 @@ class PrismaDelegate: SpeziAppDelegate {
PrismaScheduler()
OnboardingDataSource()
PrismaPushNotifications()
PrivacyModule(sampleTypeList: sampleList)
}
}

Expand All @@ -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),
Expand Down
85 changes: 85 additions & 0 deletions Prisma/PrivacyControls/DeleteDataView.swift
Original file line number Diff line number Diff line change
@@ -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<Bool>(
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")
}
47 changes: 47 additions & 0 deletions Prisma/PrivacyControls/ManageDataView.swift
Original file line number Diff line number Diff line change
@@ -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()
}
46 changes: 0 additions & 46 deletions Prisma/PrivacyControls/PrivacyControls.swift

This file was deleted.

Loading

0 comments on commit 2873e1e

Please sign in to comment.